diff --git a/reviewboard/settings.py b/reviewboard/settings.py
index e3bafdbcbd6a02694210acb64a8a9c7817e98c70..a57a9c349741be52f01bb7c001840401a6e94762 100644
--- a/reviewboard/settings.py
+++ b/reviewboard/settings.py
@@ -291,6 +291,7 @@ PIPELINE_JS = {
             'rb/js/models/tests/baseResourceModelTests.js',
             'rb/js/models/tests/diffCommentModelTests.js',
             'rb/js/models/tests/diffReviewableModelTests.js',
+            'rb/js/models/tests/fileAttachmentModelTests.js',
             'rb/js/models/tests/fileAttachmentCommentModelTests.js',
             'rb/js/models/tests/screenshotModelTests.js',
             'rb/js/models/tests/screenshotCommentModelTests.js',
@@ -318,6 +319,7 @@ PIPELINE_JS = {
             'rb/js/models/diffCommentModel.js',
             'rb/js/models/diffCommentReplyModel.js',
             'rb/js/models/diffModel.js',
+            'rb/js/models/fileAttachmentModel.js',
             'rb/js/models/fileAttachmentCommentModel.js',
             'rb/js/models/fileAttachmentCommentReplyModel.js',
             'rb/js/models/reviewGroupModel.js',
diff --git a/reviewboard/static/rb/js/common.js b/reviewboard/static/rb/js/common.js
index 82f510def47ad533dc5b1a741ae5d8b4da280691..977597ea0c14df07164746b0f9d6cbe525b3bd90 100644
--- a/reviewboard/static/rb/js/common.js
+++ b/reviewboard/static/rb/js/common.js
@@ -141,11 +141,6 @@ $.fn.formDlg = function(options) {
          * Sends the form data to the server.
          */
         function send() {
-            // TODO: Remove this when we move fully to Backbone.js.
-            if (options.dataStoreObject.setForm) {
-                options.dataStoreObject.setForm(form);
-            }
-
             options.dataStoreObject.save({
                 form: form,
                 buttons: $("input:button", box.modalBox("buttons")),
diff --git a/reviewboard/static/rb/js/datastore.js b/reviewboard/static/rb/js/datastore.js
index 3f838c8c34b52db5d3ecd6ce1224e1c6263591f0..24cd184c22f6d2bbe45bb97693696a58a46cc8ab 100644
--- a/reviewboard/static/rb/js/datastore.js
+++ b/reviewboard/static/rb/js/datastore.js
@@ -47,7 +47,10 @@ $.extend(RB.ReviewRequest.prototype, {
     },
 
     createFileAttachment: function(file_attachment_id) {
-        return new RB.FileAttachment(this, file_attachment_id);
+        return new RB.FileAttachment({
+            parentObject: this,
+            id: file_attachment_id
+        });
     },
 
     /*
@@ -454,162 +457,6 @@ $.extend(RB.Review.prototype, {
 });
 
 
-RB.FileAttachment = function(review_request, id) {
-    this.review_request = review_request;
-    this.id = id;
-    this.caption = null;
-    this.thumbnail = null;
-    this.path = null;
-    this.url = null;
-    this.loaded = false;
-
-    return this;
-};
-
-$.extend(RB.FileAttachment.prototype, {
-    setFile: function(file) {
-        this.file = file;
-    },
-
-    setForm: function(form) {
-        this.form = form;
-    },
-
-    ready: function(on_done) {
-        if (this.loaded && this.id) {
-            on_done.apply(this, arguments);
-        } else {
-            this._load(on_done);
-        }
-    },
-
-    save: function(options) {
-        options = $.extend(true, {
-            success: function() {},
-            error: function() {}
-        }, options);
-
-        if (this.id) {
-            var data = {};
-
-            if (this.caption != null) {
-                data.caption = this.caption;
-            }
-
-            var self = this;
-            this.ready(function() {
-                RB.apiCall({
-                    type: "PUT",
-                    url: self.url,
-                    data: data,
-                    buttons: options.buttons,
-                    success: function(rsp) {
-                        self._loadDataFromResponse(rsp);
-
-                        if ($.isFunction(options.success)) {
-                            options.success(rsp);
-                        }
-                    }
-                });
-            });
-        } else {
-            if (this.form) {
-                this._saveForm(options);
-            } else if (this.file) {
-                this._saveFile(options);
-            } else {
-                options.error("No data has been set for this file. " +
-                              "This is a script error. Please report it.");
-            }
-        }
-    },
-
-    deleteFileAttachment: function() {
-        var self = this;
-
-        self.ready(function() {
-            if (self.loaded) {
-                RB.apiCall({
-                    type: "DELETE",
-                    url: self.url,
-                    success: function() {
-                        $.event.trigger("deleted", null, self);
-                        self._deleteAndDestruct();
-                    }
-                });
-            }
-        });
-    },
-
-    _load: function(on_done) {
-        if (!this.id) {
-            on_done.apply(this, arguments);
-            return;
-        }
-
-        var self = this;
-
-        self.review_request.ready(function() {
-            RB.apiCall({
-                type: "GET",
-                url: self.review_request.links.file_attachments.href + self.id + "/",
-                success: function(rsp, status) {
-                    if (status != 404) {
-                        self._loadDataFromResponse(rsp);
-                    }
-
-                    on_done.apply(this, arguments);
-                }
-            });
-        });
-    },
-
-    _loadDataFromResponse: function(rsp) {
-        this.id = rsp.file_attachment.id;
-        this.caption = rsp.file_attachment.caption;
-        this.thumbnail = rsp.file_attachment.thumbnail;
-        this.path = rsp.file_attachment.path;
-        this.url = rsp.file_attachment.links.self.href;
-        this.loaded = true;
-    },
-
-    _saveForm: function(options) {
-        this._saveApiCall(options.success, options.error, {
-            buttons: options.buttons,
-            form: this.form
-        });
-    },
-
-    _saveFile: function(options) {
-        sendFileBlob(this.file, this._saveApiCall, this, options);
-    },
-
-    _saveApiCall: function(onSuccess, onError, options) {
-        var self = this;
-        self.review_request.ready(function() {
-            RB.apiCall($.extend(options, {
-                url: self.review_request.links.file_attachments.href,
-                success: function(rsp) {
-                    if (rsp.stat == "ok") {
-                        self._loadDataFromResponse(rsp);
-
-                        if ($.isFunction(onSuccess)) {
-                            onSuccess(rsp, rsp.file_attachment);
-                        }
-                    } else if ($.isFunction(onError)) {
-                        onError(rsp, rsp.err.msg);
-                    }
-                }
-            }));
-        });
-    },
-
-    _deleteAndDestruct: function() {
-        $.event.trigger("destroyed", null, this);
-    }
-});
-
-
 /*
  * Convenience wrapper for Review Board API functions. This will handle
  * any button disabling/enabling, write to the correct path prefix, form
@@ -806,43 +653,6 @@ Backbone.ajax = function(options) {
 };
 
 
-function sendFileBlob(file, save_func, obj, options) {
-    var reader = new FileReader();
-
-    reader.onloadend = function() {
-        var boundary = "-----multipartformboundary" + new Date().getTime();
-        var blob = "";
-        blob += "--" + boundary + "\r\n";
-        blob += 'Content-Disposition: form-data; name="path"; ' +
-                'filename="' + file.name + '"\r\n';
-        blob += 'Content-Type: ' + file.type + '\r\n';
-        blob += '\r\n';
-        blob += reader.result;
-        blob += '\r\n';
-        blob += "--" + boundary + "--\r\n";
-        blob += '\r\n';
-
-        save_func.call(obj, options.success, options.error, {
-            buttons: options.buttons,
-            data: blob,
-            processData: false,
-            contentType: "multipart/form-data; boundary=" + boundary,
-            xhr: function() {
-                var xhr = $.ajaxSettings.xhr();
-
-                xhr.send = function(data) {
-                    xhr.sendAsBinary(data);
-                };
-
-                return xhr;
-            }
-        });
-    };
-
-    reader.readAsBinaryString(file);
-}
-
-
 if (!XMLHttpRequest.prototype.sendAsBinary) {
     XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
         var data = new Uint8Array(
diff --git a/reviewboard/static/rb/js/models/baseResourceModel.js b/reviewboard/static/rb/js/models/baseResourceModel.js
index e8c5f64e01b69262618ebb05d50fe4af9744bd1b..800c388c578992d5198cc1bb7bc38a969d71d0a9 100644
--- a/reviewboard/static/rb/js/models/baseResourceModel.js
+++ b/reviewboard/static/rb/js/models/baseResourceModel.js
@@ -263,7 +263,10 @@ RB.BaseResource = Backbone.Model.extend({
      * readiness and creation checks of this object and its parent.
      */
     _saveObject: function(options, context) {
-        var url = _.result(this, 'url');
+        var url = _.result(this, 'url'),
+            file,
+            reader,
+            saveOptions;
 
         if (!url) {
             if (_.isFunction(options.error)) {
@@ -275,7 +278,7 @@ RB.BaseResource = Backbone.Model.extend({
             return;
         }
 
-        Backbone.Model.prototype.save.call(this, {}, _.defaults({
+        saveOptions = _.defaults({
             success: _.bind(function() {
                 if (_.isFunction(options.success)) {
                     options.success.apply(context, arguments);
@@ -287,10 +290,87 @@ RB.BaseResource = Backbone.Model.extend({
             error: _.isFunction(options.error)
                    ? _.bind(options.error, context)
                    : undefined
+        }, options);
+
+        saveOptions.attrs = options.attrs || this.toJSON(options);
+
+        if (!options.form) {
+            if (this.payloadFileKey && window.File) {
+                /* See if there's a file in the attributes we're using. */
+                file = saveOptions.attrs[this.payloadFileKey];
+            }
+        }
+
+        if (file) {
+            reader = new FileReader();
+            reader.onloadend = _.bind(function() {
+                this._saveWithFile(file, reader.result, saveOptions);
+            }, this);
+
+            reader.readAsBinaryString(file);
+        } else {
+            Backbone.Model.prototype.save.call(this, {}, saveOptions);
+        }
+    },
+
+    /*
+     * Saves the model with a file upload.
+     *
+     * When doing file uploads, we need to hand-structure a form-data payload
+     * to the server. It will contain the file contents and the attributes
+     * we're saving. We cna then call the standard save function with this
+     * payload as our data.
+     */
+    _saveWithFile: function(file, fileData, options) {
+        var boundary = options.boundary ||
+                       ('-----multipartformboundary' + new Date().getTime()),
+            blob = [];
+
+        blob.push('--' + boundary + '\r\n');
+        blob.push('Content-Disposition: form-data; name="' +
+                  this.payloadFileKey + '"; filename="' + file.name + '"\r\n');
+        blob.push('Content-Type: ' + file.type + '\r\n');
+        blob.push('\r\n');
+        blob.push(fileData);
+        blob.push('\r\n');
+
+        _.each(options.attrs, function(value, key) {
+            if (key !== this.payloadFileKey && value !== undefined &&
+                value !== null) {
+                blob.push('--' + boundary + '\r\n');
+                blob.push('Content-Disposition: form-data; name="' + key +
+                          '"\r\n');
+                blob.push('\r\n');
+                blob.push(value + '\r\n');
+            }
+        }, this);
+
+        blob.push('--' + boundary + '--\r\n\r\n');
+
+        Backbone.Model.prototype.save.call(this, {}, _.extend({
+            data: blob.join(''),
+            processData: false,
+            contentType: 'multipart/form-data; boundary=' + boundary,
+            xhr: this._binaryXHR
         }, options));
     },
 
     /*
+     * Builds a binary-capable XHR.
+     *
+     * Since we must send files as blob data, and not all XHR implementations
+     * do this by default, we must override the XHR and change which send
+     * function it will use.
+     */
+    _binaryXHR: function() {
+        var xhr = $.ajaxSettings.xhr();
+
+        xhr.send = xhr.sendAsBinary;
+
+        return xhr;
+    },
+
+    /*
      * Deletes the object's resource on the server.
      *
      * An object must either be loaded or have a parent resource linking to
@@ -394,26 +474,29 @@ RB.BaseResource = Backbone.Model.extend({
     sync: function(method, model, options) {
         options = options || {};
 
-        return Backbone.sync.call(this, method, model, _.defaults({
+        return Backbone.sync.call(this, method, model, _.defaults({}, options, {
             /* Use form data instead of a JSON payload. */
             contentType: 'application/x-www-form-urlencoded',
-            data: options.attrs || model.toJSON(options),
+            data: options.form ? null
+                               : (options.attrs || model.toJSON(options)),
             processData: true,
 
             error: function(xhr, textStatus, errorThrown) {
-                var rsp = null,
-                    text;
-
-                try {
-                    rsp = $.parseJSON(xhr.responseText);
-                    text = rsp.err.msg;
-                } catch (e) {
-                    text = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
-                }
+                if (_.isFunction(options.error)) {
+                    var rsp = null,
+                        text;
+
+                    try {
+                        rsp = $.parseJSON(xhr.responseText);
+                        text = rsp.err.msg;
+                    } catch (e) {
+                        text = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
+                    }
 
-                options.error(model, text, xhr.statusText);
+                    options.error(model, text, xhr.statusText);
+                }
             }
-        }, options));
+        }));
     }
 }, {
     strings: {
diff --git a/reviewboard/static/rb/js/models/fileAttachmentModel.js b/reviewboard/static/rb/js/models/fileAttachmentModel.js
new file mode 100644
index 0000000000000000000000000000000000000000..487399cc5e38bcd18e1209a92e378a2e5d4c21be
--- /dev/null
+++ b/reviewboard/static/rb/js/models/fileAttachmentModel.js
@@ -0,0 +1,63 @@
+/*
+ * A new or existing file attachment.
+ *
+ */
+RB.FileAttachment = RB.BaseResource.extend({
+    defaults: _.defaults({
+        /* The file attachment's caption. */
+        caption: null,
+
+        /* The URL to download a file, for existing file attachments. */
+        downloadURL: null,
+
+        /* The file to upload. Only works for newly created FileAttachments. */
+        file: null,
+
+        /* The name of the file, for existing file attachments. */
+        filename: null,
+
+        /* The URL to an icon for this file type. */
+        iconURL: null,
+
+        /* The URL to the review UI for this file attachment. */
+        reviewURL: null,
+
+        /* The HTML for the thumbnail depicting this file attachment. */
+        thumbnailHTML: null
+    }, RB.BaseResource.prototype.defaults),
+
+    rspNamespace: 'file_attachment',
+    payloadFileKey: 'path',
+
+    /*
+     * Serializes the changes to the file attachment to a payload.
+     */
+    toJSON: function() {
+        var payload = {
+            caption: this.get('caption')
+        };
+
+        if (this.isNew()) {
+            payload.path = this.get('file');
+        }
+
+        return payload;
+    },
+
+    /*
+     * Deserializes a file attachment data from an API payload.
+     */
+    parse: function(rsp) {
+        var result = RB.BaseResource.prototype.parse.call(this, rsp),
+            rspData = rsp[this.rspNamespace];
+
+        result.caption = rspData.caption;
+        result.downloadURL = rspData.url;
+        result.filename = rspData.filename;
+        result.iconURL = rspData.icon_url;
+        result.reviewURL = rspData.review_url;
+        result.thumbnailHTML = rspData.thumbnail;
+
+        return result;
+    }
+});
diff --git a/reviewboard/static/rb/js/models/tests/baseResourceModelTests.js b/reviewboard/static/rb/js/models/tests/baseResourceModelTests.js
index 82ab52c61bcc84a645cae55afd45e75dd45bbd43..4125d855c1ccacd4338793275971aa71cc6cd376 100644
--- a/reviewboard/static/rb/js/models/tests/baseResourceModelTests.js
+++ b/reviewboard/static/rb/js/models/tests/baseResourceModelTests.js
@@ -636,6 +636,173 @@ describe('models/BaseResource', function() {
                 expect($.ajax).toHaveBeenCalled();
             });
         });
+
+        describe('With file upload support', function() {
+            beforeEach(function() {
+                model.payloadFileKey = 'file';
+                model.url = '/api/foos/';
+                model.toJSON = function(options) {
+                    return {
+                        file: this.get('file'),
+                        myfield: 'myvalue'
+                    };
+                };
+
+                spyOn(Backbone.Model.prototype, 'save').andCallThrough();
+                spyOn(RB, 'apiCall').andCallThrough();
+            });
+
+            it('With file', function() {
+                var seenComplete = false,
+                    boundary = '-----multipartformboundary';
+
+                runs(function() {
+                    var blob = new Blob(['Hello world!'], {
+                        type: 'text/plain'
+                    });
+                    blob.name = 'myfile';
+
+                    spyOn($, 'ajax').andCallFake(function(request) {
+                        expect(request.type).toBe('POST');
+                        expect(request.processData).toBe(false);
+                        expect(request.contentType.indexOf(
+                            'multipart/form-data; boundary=')).toBe(0);
+                        expect(request.data).toBe(
+                            '--' + boundary + '\r\n' +
+                            'Content-Disposition: form-data; name="file"' +
+                            '; filename="myfile"\r\n' +
+                            'Content-Type: text/plain\r\n\r\n' +
+                            'Hello world!' +
+                            '\r\n' +
+                            '--' + boundary + '\r\n' +
+                            'Content-Disposition: form-data; ' +
+                            'name="myfield"\r\n\r\n' +
+                            'myvalue\r\n' +
+                            '--' + boundary + '--\r\n\r\n');
+
+                        request.success({
+                            stat: 'ok',
+                            foo: {
+                                id: 42
+                            }
+                        });
+                    });
+
+                    model.set('file', blob);
+                    model.save({
+                        success: function() {
+                            seenComplete = true;
+                        },
+                        boundary: boundary
+                    });
+                });
+
+                waitsFor(function() {
+                    return seenComplete;
+                });
+
+                runs(function() {
+                    expect(Backbone.Model.prototype.save).toHaveBeenCalled();
+                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect($.ajax).toHaveBeenCalled();
+                });
+            });
+
+            it('Without file', function() {
+                runs(function() {
+                    spyOn($, 'ajax').andCallFake(function(request) {
+                        expect(request.type).toBe('POST');
+                        expect(request.processData).toBe(true);
+                        expect(request.contentType).toBe(
+                            'application/x-www-form-urlencoded');
+
+                        request.success({
+                            stat: 'ok',
+                            foo: {
+                                id: 42
+                            }
+                        });
+                    });
+
+                    model.save({
+                        success: function() {
+                            seenComplete = true;
+                        }
+                    });
+                });
+
+                waitsFor(function() {
+                    return seenComplete;
+                });
+
+                runs(function() {
+                    expect(Backbone.Model.prototype.save).toHaveBeenCalled();
+                    expect(RB.apiCall).toHaveBeenCalled();
+                    expect($.ajax).toHaveBeenCalled();
+                });
+            });
+        });
+
+        describe('With form upload support', function() {
+            beforeEach(function() {
+                model.url = '/api/foos/';
+            });
+
+            it('Overriding toJSON attributes', function() {
+                var form = $('<form/>')
+                    .append($('<input name="foo"/>'));
+
+                model.toJSON = function(options) {
+                    return {
+                        myfield: 'myvalue'
+                    };
+                };
+
+                spyOn(Backbone, 'sync').andCallThrough();
+                spyOn(RB, 'apiCall').andCallThrough();
+                spyOn($, 'ajax');
+                spyOn(form, 'ajaxSubmit');
+
+                model.save({
+                    form: form
+                });
+
+                expect(RB.apiCall).toHaveBeenCalled();
+                expect(form.ajaxSubmit).toHaveBeenCalled();
+                expect($.ajax).not.toHaveBeenCalled();
+                expect(Backbone.sync.calls[0].args[2].data).toBe(null);
+                expect(RB.apiCall.calls[0].args[0].data).toBe(null);
+            });
+
+            it('Overriding file attributes', function() {
+                var form = $('<form/>')
+                    .append($('<input name="foo"/>'));
+
+                model.payloadFileKey = 'file';
+                model.toJSON = function(options) {
+                    return {
+                        file: this.get('file')
+                    };
+                };
+
+                spyOn(model, '_saveWithFile').andCallThrough();
+                spyOn(Backbone, 'sync').andCallThrough();
+                spyOn(RB, 'apiCall').andCallThrough();
+                spyOn($, 'ajax');
+                spyOn(form, 'ajaxSubmit');
+
+                model.save({
+                    form: form
+                });
+
+                expect(model._saveWithFile).not.toHaveBeenCalled();
+                expect(RB.apiCall).toHaveBeenCalled();
+                expect(form.ajaxSubmit).toHaveBeenCalled();
+                expect($.ajax).not.toHaveBeenCalled();
+                expect(Backbone.sync.calls[0].args[2].data).toBe(null);
+                expect(RB.apiCall.calls[0].args[0].data).toBe(null);
+            });
+        });
     });
 
     describe('url', function() {
diff --git a/reviewboard/static/rb/js/models/tests/fileAttachmentModelTests.js b/reviewboard/static/rb/js/models/tests/fileAttachmentModelTests.js
new file mode 100644
index 0000000000000000000000000000000000000000..c63b9df2e2b1cc6cafc1df8669206d369e204674
--- /dev/null
+++ b/reviewboard/static/rb/js/models/tests/fileAttachmentModelTests.js
@@ -0,0 +1,71 @@
+describe('models/FileAttachment', function() {
+    beforeEach(function() {
+        parentObject = new RB.BaseResource({
+            public: true
+        });
+
+        model = new RB.FileAttachment({
+            parentObject: parentObject
+        });
+    });
+
+    describe('toJSON', function() {
+        describe('caption field', function() {
+            it('With value', function() {
+                var data;
+
+                model.set('caption', 'foo');
+                data = model.toJSON();
+                expect(data.caption).toBe('foo');
+            });
+        });
+
+        describe('file field', function() {
+            it('With new file attachment', function() {
+                var data;
+
+                expect(model.isNew()).toBe(true);
+
+                model.set('file', 'abc');
+                data = model.toJSON();
+                expect(data.path).toBe('abc');
+            });
+
+            it('With existing file attachment', function() {
+                var data;
+
+                model.id = 123;
+                expect(model.isNew()).toBe(false);
+
+                model.set('file', 'abc');
+                data = model.toJSON();
+                expect(data.path).toBe(undefined);
+            });
+        });
+    });
+
+    describe('parse', function() {
+        it('API payloads', function() {
+            var data = model.parse({
+                file_attachment: {
+                    id: 42,
+                    caption: 'caption',
+                    url: 'downloadURL',
+                    filename: 'filename',
+                    icon_url: 'iconURL',
+                    review_url: 'reviewURL',
+                    thumbnail: 'thumbnailHTML'
+                }
+            });
+
+            expect(data).not.toBe(undefined);
+            expect(data.id).toBe(42);
+            expect(data.caption).toBe('caption');
+            expect(data.downloadURL).toBe('downloadURL');
+            expect(data.filename).toBe('filename');
+            expect(data.iconURL).toBe('iconURL');
+            expect(data.reviewURL).toBe('reviewURL');
+            expect(data.thumbnailHTML).toBe('thumbnailHTML');
+        });
+    });
+});
diff --git a/reviewboard/static/rb/js/reviews.js b/reviewboard/static/rb/js/reviews.js
index f03b6974713b461ab0cb7e1f91365943a2985eb7..025f6b9b24e6648f8e6bfc2ae93d9192abe02ddb 100644
--- a/reviewboard/static/rb/js/reviews.js
+++ b/reviewboard/static/rb/js/reviews.js
@@ -1195,14 +1195,16 @@ $.fn.fileAttachment = function() {
                     } else {
                         $(this).removeClass("empty-caption");
                     }
-                    fileAttachment.ready(function() {
-                        fileAttachment.caption = value;
-                        fileAttachment.save({
-                            buttons: gDraftBannerButtons,
-                            success: function(rsp) {
-                                gDraftBanner.show();
-                            }
-                        });
+                    fileAttachment.ready({
+                        ready: function() {
+                            fileAttachment.set('caption', value);
+                            fileAttachment.save({
+                                buttons: gDraftBannerButtons,
+                                success: function(rsp) {
+                                    gDraftBanner.show();
+                                }
+                            });
+                        }
                     });
                 }
             });
@@ -1222,10 +1224,15 @@ $.fn.fileAttachment = function() {
 
         self.find("a.delete")
             .click(function() {
-                fileAttachment.ready(function() {
-                    fileAttachment.deleteFileAttachment()
-                    self.empty();
-                    gDraftBanner.show();
+                fileAttachment.ready({
+                    ready: function() {
+                        fileAttachment.destroy({
+                            success: function() {
+                                self.empty();
+                                gDraftBanner.show();
+                            }
+                        });
+                    }
                 });
 
                 return false;
@@ -1378,14 +1385,14 @@ $.newFileAttachment = function(fileAttachment) {
         attachments = $("#file-list");
 
     container = $(newFileAttachmentTemplate({
-        caption: fileAttachment.caption,
+        caption: fileAttachment.get('caption'),
         delete_image_url: STATIC_URLS['rb/images/delete.png'],
-        filename: fileAttachment.filename,
-        icon_url: fileAttachment.icon_url,
+        filename: fileAttachment.get('filename'),
+        icon_url: fileAttachment.get('iconURL'),
         id: fileAttachment.id,
-        review_url: fileAttachment.review_url,
-        thumbnail: fileAttachment.thumbnail,
-        url: fileAttachment.url
+        review_url: fileAttachment.get('reviewURL'),
+        thumbnail: fileAttachment.get('thumbnailHTML'),
+        url: fileAttachment.get('downloadURL')
     }));
 
     container.fileAttachment();
diff --git a/reviewboard/static/rb/js/views/dndUploaderView.js b/reviewboard/static/rb/js/views/dndUploaderView.js
index 2bf09aace9247479a073e3ef6af415e6defe7a24..33386886229480a08ffa2f6aa18339989575247e 100644
--- a/reviewboard/static/rb/js/views/dndUploaderView.js
+++ b/reviewboard/static/rb/js/views/dndUploaderView.js
@@ -168,10 +168,10 @@ RB.DnDUploader = Backbone.View.extend({
                 .css('opacity', 0)
                 .fadeTo(1000, 1);
 
-        fileAttachment.setFile(file);
+        fileAttachment.set('file', file);
         fileAttachment.save({
             buttons: RB.draftBannerButtons,
-            success: function(rsp, fileAttachment) {
+            success: function() {
                 $thumb.replaceWith($.newFileAttachment(fileAttachment));
                 RB.draftBanner.show();
             },
diff --git a/reviewboard/templates/reviews/review_request_dlgs.html b/reviewboard/templates/reviews/review_request_dlgs.html
index b0f7088d02c3abe0b5ce6b5c4cbb45c7d722dadc..bffbe22fc38fcc07175074d14f40ff5519ecdc8a 100644
--- a/reviewboard/templates/reviews/review_request_dlgs.html
+++ b/reviewboard/templates/reviews/review_request_dlgs.html
@@ -22,16 +22,18 @@
 {%  endif %}
 
     $("#upload-file-link").click(function() {
+      var fileAttachment = gReviewRequest.createFileAttachment();
+
       $("<div/>").formDlg({
         title: "{% trans "Upload File" %}",
         confirmLabel: "{% trans "Upload" %}",
-        dataStoreObject: gReviewRequest.createFileAttachment(),
+        dataStoreObject: fileAttachment,
         width: "50em",
         upload: true,
         fields: {% form_dialog_fields file_attachment_form %},
         success: function(rsp) {
             if (!$("#file-list").length == 0) {
-                $.newFileAttachment(rsp.file_attachment);
+                $.newFileAttachment(fileAttachment);
             }
 
             RB.draftBanner.show();
