MozReview: Created copies of files to be edited. (Bug 1259237). draft
authorBrian Birkhauser <bcbirkhauser@gmail.com>
Wed, 07 Sep 2016 14:50:04 -0700
changeset 105 eec761ad8cec35e7a669fade637aa7bfd36e7b2a
parent 99 f8aedfca0322ae9be1bacbaee7de600484ec6f1d
child 106 bfab041438b5cd58f56c44034a01fcc5670a3336
push idunknown
push userunknown
push dateunknown
bugs1259237
MozReview: Created copies of files to be edited. (Bug 1259237). MozReview-Commit-ID: VrEGdRVqQd
reviewboard/reviewboard/static/rb/js/diffviewer/models/diffReviewableModel_mozreview.js
reviewboard/reviewboard/static/rb/js/resources/models/baseCommentModel_mozreview.js
reviewboard/reviewboard/static/rb/js/resources/models/diffCommentModel_mozreview.js
reviewboard/reviewboard/static/rb/js/resources/models/draftReviewModel_mozreview.js
reviewboard/reviewboard/static/rb/js/resources/models/reviewRequestModel_mozreview.js
reviewboard/reviewboard/staticbundles.py
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/diffviewer/models/diffReviewableModel_mozreview.js
@@ -0,0 +1,103 @@
+/*
+ * Provides state and utility functions for loading and reviewing diffs.
+ */
+RB.DiffReviewable = RB.AbstractReviewable.extend({
+    defaults: _.defaults({
+        fileIndex: null,
+        fileDiffID: null,
+        interFileDiffID: null,
+        revision: null,
+        interdiffRevision: null
+    }, RB.AbstractReviewable.prototype.defaults),
+
+    commentBlockModel: RB.DiffCommentBlock,
+    defaultCommentBlockFields: ['fileDiffID', 'interFileDiffID'],
+
+    /*
+     * Adds comment blocks for the serialized comment blocks passed to the
+     * reviewable.
+     */
+    loadSerializedCommentBlock: function(serializedCommentBlock) {
+        this.createCommentBlock({
+            reviewRequest: this.get('reviewRequest'),
+            review: this.get('review'),
+            fileDiffID: this.get('fileDiffID'),
+            interFileDiffID: this.get('interFileDiffID'),
+            beginLineNum: serializedCommentBlock.linenum,
+            endLineNum: serializedCommentBlock.linenum +
+                        serializedCommentBlock.num_lines - 1,
+            serializedComments: serializedCommentBlock.comments || []
+        });
+    },
+
+    /*
+     * Returns the rendered diff for a file.
+     *
+     * The rendered file will be fetched from the server and eventually
+     * returned as the argument to the success callback.
+     */
+    getRenderedDiff: function(callbacks, context) {
+        this._fetchFragment({
+            url: this._buildRenderedDiffURL() +
+                 '?index=' + this.get('fileIndex') + '&' + TEMPLATE_SERIAL,
+            noActivityIndicator: true
+        }, callbacks, context);
+    },
+
+    /*
+     * Returns a rendered fragment of a diff.
+     *
+     * The fragment will be fetched from the server and eventually returned
+     * as the argument to the success callback.
+     */
+    getRenderedDiffFragment: function(options, callbacks, context) {
+        console.assert(options.chunkIndex !== undefined,
+                       'chunkIndex must be provided');
+
+        this._fetchFragment({
+            url: this._buildRenderedDiffURL() + 'chunk/' +
+                 options.chunkIndex + '/',
+            data: {
+                'index': this.get('fileIndex'),
+                'lines-of-context': options.linesOfContext
+            }
+        }, callbacks, context);
+    },
+
+    /*
+     * Fetches the diff fragment from the server.
+     *
+     * This is used internally by getRenderedDiff and getRenderedDiffFragment
+     * to do all the actual fetching and calling of callbacks.
+     */
+    _fetchFragment: function(options, callbacks, context) {
+        RB.apiCall(_.defaults(
+            {
+                type: 'GET',
+                dataType: 'html'
+            },
+            options,
+            _.bindCallbacks(callbacks, context)
+        ));
+    },
+
+    /*
+     * Builds a URL that forms the base of a diff fragment fetch.
+     */
+    _buildRenderedDiffURL: function() {
+        var revisionStr,
+            interdiffRevision = this.get('interdiffRevision'),
+            interFileDiffID = this.get('interFileDiffID');
+
+        revisionStr = this.get('revision');
+
+        if (interdiffRevision) {
+            revisionStr += '-' + interdiffRevision;
+        }
+
+        return this.get('reviewRequest').get('reviewURL') + 'diff/' +
+               revisionStr + '/fragment/' + this.get('fileDiffID') +
+               (interFileDiffID ? '-' + interFileDiffID : '') +
+               '/';
+    }
+});
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/resources/models/baseCommentModel_mozreview.js
@@ -0,0 +1,185 @@
+/*
+ * The base model for a comment.
+ *
+ * This provides all the common properties, serialization, deserialization,
+ * validation, and other functionality of comments. It's meant to be
+ * subclassed by more specific implementations.
+ */
+RB.BaseComment = RB.BaseResource.extend({
+    defaults: function() {
+        return _.defaults({
+            /*
+             * The text format type to request for text in all responses.
+             */
+            forceTextType: null,
+
+            /*
+             * A string containing a comma-separated list of text types to
+             * include in the payload.
+             */
+            includeTextTypes: null,
+
+            /* Whether or not an issue is opened. */
+            issueOpened: null,
+
+            /*
+             * The current state of the issue.
+             *
+             * This must be one of STATE_DROPPED, STATE_OPEN, or
+             * STATE_RESOLVED.
+             */
+            issueStatus: null,
+
+            /*
+             * Markdown-formatted text fields, if the caller fetches or posts
+             * with include-text-types=markdown.
+             */
+            markdownTextFields: {},
+
+            /*
+             * Raw text fields, if the caller fetches or posts with
+             * include-text-types=raw.
+             */
+            rawTextFields: {},
+
+            /* Whether the comment is saved in rich-text (Markdown) format. */
+            richText: null,
+
+            /* The text entered for the comment. */
+            text: ''
+        }, RB.BaseResource.prototype.defaults());
+    },
+
+    extraQueryArgs: function() {
+        var textTypes = 'raw';
+
+        if (RB.UserSession.instance.get('defaultUseRichText')) {
+            textTypes += ',markdown';
+        }
+
+        return {
+            'force-text-type': 'html',
+            'include-text-types': textTypes
+        };
+    },
+
+    supportsExtraData: true,
+
+    attrToJsonMap: {
+        forceTextType: 'force_text_type',
+        includeTextTypes: 'include_text_types',
+        issueOpened: 'issue_opened',
+        issueStatus: 'issue_status',
+        richText: 'text_type'
+    },
+
+    serializedAttrs: [
+        'forceTextType',
+        'includeTextTypes',
+        'issueOpened',
+        'issueStatus',
+        'richText',
+        'text'
+    ],
+
+    deserializedAttrs: [
+        'issueOpened',
+        'issueStatus',
+        'text',
+        'html'
+    ],
+
+    serializers: {
+        forceTextType: RB.JSONSerializers.onlyIfValue,
+        includeTextTypes: RB.JSONSerializers.onlyIfValue,
+        richText: RB.JSONSerializers.textType,
+
+        issueStatus: function(value) {
+            var parentObject;
+
+            if (this.get('loaded')) {
+                parentObject = this.get('parentObject');
+
+                if (parentObject.get('public')) {
+                    return value;
+                }
+            }
+
+            return undefined;
+        }
+    },
+
+    /*
+     * Destroys the comment if and only if the text is empty.
+     *
+     * This works just like destroy(), and will in fact call destroy()
+     * with all provided arguments, but only if there's some actual
+     * text in the comment.
+     */
+    destroyIfEmpty: function(options, context) {
+        if (!this.get('text')) {
+            this.destroy(options, context);
+        }
+    },
+
+    /*
+     * Deserializes comment data from an API payload.
+     *
+     * This must be overloaded by subclasses, and the parent version called.
+     */
+    parseResourceData: function(rsp) {
+        var rawTextFields = rsp.raw_text_fields || rsp,
+            data = RB.BaseResource.prototype.parseResourceData.call(this, rsp);
+
+        data.richText = (rawTextFields.text_type === 'markdown');
+
+        if (rsp.raw_text_fields) {
+            data.rawTextFields = {
+                text: rsp.raw_text_fields.text
+            };
+        }
+
+        if (rsp.markdown_text_fields) {
+            data.markdownTextFields = {
+                text: rsp.markdown_text_fields.text
+            };
+        }
+
+        if (rsp.html_text_fields) {
+            data.html = rsp.html_text_fields.text;
+        }
+
+        return data;
+    },
+
+    /*
+     * Performs validation on the attributes of the model.
+     *
+     * By default, this validates the issueStatus field. It can be
+     * overridden to provide additional validation, but the parent
+     * function must be called.
+     */
+    validate: function(attrs) {
+        if (_.has(attrs, 'parentObject') && !attrs.parentObject) {
+            return RB.BaseResource.strings.UNSET_PARENT_OBJECT;
+        }
+
+        if (attrs.issueStatus &&
+            attrs.issueStatus !== RB.BaseComment.STATE_DROPPED &&
+            attrs.issueStatus !== RB.BaseComment.STATE_OPEN &&
+            attrs.issueStatus !== RB.BaseComment.STATE_RESOLVED) {
+            return RB.BaseComment.strings.INVALID_ISSUE_STATUS;
+        }
+
+        return RB.BaseResource.prototype.validate.apply(this, arguments);
+    }
+}, {
+    STATE_DROPPED: 'dropped',
+    STATE_OPEN: 'open',
+    STATE_RESOLVED: 'resolved',
+
+    strings: {
+        INVALID_ISSUE_STATUS: 'issueStatus must be one of STATE_DROPPED, ' +
+                              'STATE_OPEN, or STATE_RESOLVED'
+    }
+});
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/resources/models/diffCommentModel_mozreview.js
@@ -0,0 +1,148 @@
+(function() {
+
+
+var parentProto = RB.BaseComment.prototype;
+
+
+/*
+ * Provides commenting functionality for diffs.
+ *
+ * A DiffComment represents a comment on a range of lines on either a
+ * FileDiff or an interdiff consisting of two FileDiffs.
+ */
+RB.DiffComment = RB.BaseComment.extend({
+    defaults: function() {
+        return _.defaults({
+            /* The first line number in the range (0-indexed). */
+            beginLineNum: 0,
+
+            /* The last line number in the range (0-indexed). */
+            endLineNum: 0,
+
+            /* The FileDiff the comment applies to. */
+            fileDiff: null,
+
+            /* The ID of the FileDiff the comment is on. */
+            fileDiffID: null,
+
+            /* The optional FileDiff at the end of an interdiff range. */
+            interFileDiff: null,
+
+            /*
+             * The ID of the optional FileDiff specifying the end of an
+             * interdiff range.
+             */
+            interFileDiffID: null
+        }, parentProto.defaults());
+    },
+
+    rspNamespace: 'diff_comment',
+    expandedFields: ['filediff', 'interfilediff'],
+
+    attrToJsonMap: _.defaults({
+        fileDiffID: 'filediff_id',
+        beginLineNum: 'first_line',
+        interFileDiffID: 'interfilediff_id',
+        numLines: 'num_lines'
+    }, parentProto.attrToJsonMap),
+
+    serializedAttrs: [
+        'beginLineNum',
+        'numLines',
+        'fileDiffID',
+        'interFileDiffID'
+    ].concat(parentProto.serializedAttrs),
+
+    deserializedAttrs: [
+        'beginLineNum',
+        'endLineNum'
+    ].concat(parentProto.deserializedAttrs),
+
+    serializers: _.defaults({
+        fileDiffID: RB.JSONSerializers.onlyIfUnloaded,
+        interFileDiffID: RB.JSONSerializers.onlyIfUnloadedAndValue,
+
+        numLines: function() {
+            return this.getNumLines();
+        }
+    }, parentProto.serializers),
+
+    /*
+     * Returns the total number of lines the comment spans.
+     */
+    getNumLines: function() {
+        return this.get('endLineNum') - this.get('beginLineNum') + 1;
+    },
+
+    /*
+     * Deserializes comment data from an API payload.
+     */
+    parseResourceData: function(rsp) {
+        var result = parentProto.parseResourceData.call(this, rsp);
+
+        result.endLineNum = rsp.num_lines + result.beginLineNum - 1;
+
+        result.fileDiff = new RB.FileDiff(rsp.filediff, {
+            parse: true
+        });
+
+        if (rsp.interfilediff) {
+            result.interFileDiff = new RB.FileDiff(rsp.interfilediff, {
+                parse: true
+            });
+        }
+
+        return result;
+    },
+
+    /*
+     * Performs validation on the attributes of the model.
+     *
+     * This will check the range of line numbers to make sure they're
+     * a valid ordered range, along with the default comment validation.
+     */
+    validate: function(attrs, options) {
+        var strings = RB.DiffComment.strings,
+            hasBeginLineNum,
+            hasEndLineNum;
+
+        /*
+         * XXX: Existing diff comments won't have the "fileDiffID" attribute
+         * populated when we load the object from the API. Since we don't do
+         * anything that needs that attribute unless we're trying to create a
+         * new diff comment, only check it if isNew().
+         */
+        if (this.isNew() && _.has(attrs, 'fileDiffID') && !attrs.fileDiffID) {
+            return strings.INVALID_FILEDIFF_ID;
+        }
+
+        hasBeginLineNum = _.has(attrs, 'beginLineNum');
+
+        if (hasBeginLineNum && attrs.beginLineNum < 0) {
+            return strings.BEGINLINENUM_GTE_0;
+        }
+
+        hasEndLineNum = _.has(attrs, 'endLineNum');
+
+        if (hasEndLineNum && attrs.endLineNum < 0) {
+            return strings.ENDLINENUM_GTE_0;
+        }
+
+        if (hasBeginLineNum && hasEndLineNum &&
+            attrs.beginLineNum > attrs.endLineNum) {
+            return strings.BEGINLINENUM_LTE_ENDLINENUM;
+        }
+
+        return parentProto.validate.call(this, attrs, options);
+    }
+}, {
+    strings: {
+        INVALID_FILEDIFF_ID: 'fileDiffID must be a valid ID',
+        BEGINLINENUM_GTE_0: 'beginLineNum must be >= 0',
+        ENDLINENUM_GTE_0: 'endLineNum must be >= 0',
+        BEGINLINENUM_LTE_ENDLINENUM: 'beginLineNum must be <= endLineNum'
+    }
+});
+
+
+})();
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/resources/models/draftReviewModel_mozreview.js
@@ -0,0 +1,47 @@
+/*
+ * Draft reviews.
+ *
+ * Draft reviews are more complicated than most objects. A draft may already
+ * exist on the server, in which case we need to be able to get its ID. A
+ * special resource exists at /reviews/draft/ which will redirect to the
+ * existing draft if one exists, and return 404 if not.
+ */
+RB.DraftReview = RB.Review.extend(_.extend({
+    /*
+     * Publishes the review.
+     *
+     * Before publish, the "publishing" event will be triggered.
+     *
+     * After the publish has succeeded, the "published" event will be
+     * triggered.
+     */
+    publish: function(options, context) {
+        options = options || {};
+
+        this.trigger('publishing');
+
+        this.ready({
+            ready: function() {
+                this.set('public', true);
+                this.save({
+                    attrs: options.attrs,
+                    success: function() {
+                        this.trigger('published');
+
+                        if (_.isFunction(options.success)) {
+                            options.success.call(context);
+                        }
+                    },
+                    error: function(model, xhr) {
+                        model.trigger('publishError', xhr.errorText);
+
+                        if (_.isFunction(options.error)) {
+                            options.error.call(context, model, xhr);
+                        }
+                    }
+                }, this);
+            },
+            error: error
+        }, this);
+    }
+}, RB.DraftResourceModelMixin));
new file mode 100644
--- /dev/null
+++ b/reviewboard/reviewboard/static/rb/js/resources/models/reviewRequestModel_mozreview.js
@@ -0,0 +1,427 @@
+/*
+ * A review request.
+ *
+ * ReviewRequest is the starting point for much of the resource API. Through
+ * it, the caller can create drafts, diffs, file attachments, and screenshots.
+ *
+ * Fields on a ReviewRequest are set by accessing the ReviewRequest.draft
+ * object. Through there, fields can be set like any other model and then
+ * saved.
+ *
+ * A review request can be closed by using the close() function, reopened
+ * through reopen(), or even permanently destroyed by calling destroy().
+ */
+RB.ReviewRequest = RB.BaseResource.extend({
+    defaults: function() {
+        return _.defaults({
+            approved: false,
+            approvalFailure: null,
+            branch: null,
+            bugTrackerURL: null,
+            bugsClosed: null,
+            commitID: null,
+            closeDescription: null,
+            closeDescriptionRichText: false,
+            dependsOn: [],
+            description: null,
+            descriptionRichText: false,
+            draftReview: null,
+            lastUpdated: null,
+            localSitePrefix: null,
+            'public': null,
+            repository: null,
+            reviewURL: null,
+            state: null,
+            summary: null,
+            targetGroups: [],
+            targetPeople: [],
+            testingDone: null,
+            testingDoneRichText: false
+        }, RB.BaseResource.prototype.defaults());
+    },
+
+    rspNamespace: 'review_request',
+
+    extraQueryArgs: {
+        'force-text-type': 'html',
+        'include-text-types': 'raw'
+    },
+
+    attrToJsonMap: {
+        approvalFailure: 'approval_failure',
+        bugsClosed: 'bugs_closed',
+        closeDescription: 'close_description',
+        closeDescriptionRichText: 'close_description_text_type',
+        dependsOn: 'depends_on',
+        descriptionRichText: 'description_text_type',
+        lastUpdated: 'last_updated',
+        reviewURL: 'url',
+        targetGroups: 'target_groups',
+        targetPeople: 'target_people',
+        testingDone: 'testing_done',
+        testingDoneRichText: 'testing_done_text_type'
+    },
+
+    deserializedAttrs: [
+        'approved',
+        'approvalFailure',
+        'branch',
+        'bugsClosed',
+        'closeDescription',
+        'dependsOn',
+        'description',
+        'lastUpdated',
+        'public',
+        'reviewURL',
+        'summary',
+        'targetGroups',
+        'targetPeople',
+        'testingDone'
+    ],
+
+    initialize: function(attrs, options) {
+        options = options || {};
+
+        RB.BaseResource.prototype.initialize.call(this, attrs, options);
+
+        this.reviews = new Backbone.Collection([], {
+            model: RB.Review
+        });
+
+        this.draft = new RB.DraftReviewRequest(_.defaults({
+            parentObject: this,
+            branch: this.get('branch'),
+            bugsClosed: this.get('bugsClosed'),
+            dependsOn: this.get('dependsOn'),
+            description: this.get('description'),
+            descriptionRichText: this.get('descriptionRichText'),
+            summary: this.get('summary'),
+            targetGroups: this.get('targetGroups'),
+            targetPeople: this.get('targetPeople'),
+            testingDone: this.get('testingDone'),
+            testingDoneRichText: this.get('testingDoneRichText')
+        }, options.extraDraftAttrs));
+    },
+
+    url: function() {
+        var url = SITE_ROOT + (this.get('localSitePrefix') || '') +
+                  'api/review-requests/';
+
+        if (!this.isNew()) {
+            url += this.id + '/';
+        }
+
+        return url;
+    },
+
+    /*
+     * Creates the review request from an existing commit.
+     *
+     * This can only be used for new ReviewRequest instances, and requires
+     * a commitID option.
+     */
+    createFromCommit: function(options, context) {
+        console.assert(options.commitID);
+        console.assert(this.isNew());
+
+        this.set('commitID', options.commitID);
+        this.save(
+            _.extend({
+                createFromCommit: true
+            }, options),
+            context);
+    },
+
+    /*
+     * Creates a Diff object for this review request.
+     */
+    createDiff: function() {
+        return new RB.Diff({
+            parentObject: this
+        });
+    },
+
+    /*
+     * Creates a Review object for this review request.
+     *
+     * If an ID is specified, the Review object will reference that ID.
+     * Otherwise, it is considered a draft review, and will either return
+     * the existing one (if the draftReview attribute is set), or create
+     * a new one (and set the attribute).
+     */
+    createReview: function(reviewID) {
+        var review;
+
+        if (reviewID === undefined) {
+            review = this.get('draftReview');
+
+            if (review === null) {
+                review = new RB.DraftReview({
+                    parentObject: this
+                });
+
+                this.set('draftReview', review);
+            }
+
+            return review;
+        } else {
+            review = this.reviews.get(reviewID);
+
+            if (!review) {
+                review = new RB.Review({
+                    parentObject: this,
+                    id: reviewID
+                });
+                this.reviews.add(review);
+            }
+        }
+
+        return review;
+    },
+
+    /*
+     * Creates a Screenshot object for this review request.
+     */
+    createScreenshot: function(screenshotID) {
+        return new RB.Screenshot({
+            parentObject: this,
+            id: screenshotID
+        });
+    },
+
+    /*
+     * Creates a FileAttachment object for this review request.
+     */
+    createFileAttachment: function(attributes) {
+        return new RB.FileAttachment(_.defaults({
+            parentObject: this
+        }, attributes));
+    },
+
+    /*
+     * Marks a review request as starred or unstarred.
+     */
+    setStarred: function(starred, options, context) {
+        var watched = RB.UserSession.instance.watchedReviewRequests;
+
+        if (starred) {
+            watched.addImmediately(this, options, context);
+        } else {
+            watched.removeImmediately(this, options, context);
+        }
+    },
+
+    /*
+     * Closes the review request.
+     *
+     * A 'type' option must be provided, which must match one of the
+     * close types (ReviewRequest.CLOSE_DISCARDED or
+     * ReviewRequest.CLOSE_SUBMITTED).
+     *
+     * An optional description can be set by passing a 'description' option.
+     */
+    close: function(options, context) {
+        var data = {},
+            changingState,
+            saveOptions;
+
+        console.assert(options);
+
+        if (options.type === RB.ReviewRequest.CLOSE_DISCARDED) {
+            data.status = 'discarded';
+        } else if (options.type === RB.ReviewRequest.CLOSE_SUBMITTED) {
+            data.status = 'submitted';
+        } else {
+            if (_.isFunction(options.error)) {
+                options.error.call(this, {
+                    errorText: 'Invalid close type'
+                });
+            }
+
+            return;
+        }
+
+        if (options.description !== undefined) {
+            data.close_description = options.description;
+        }
+
+        if (options.richText !== undefined) {
+            data.close_description_text_type =
+                (options.richText ? 'markdown' : 'plain');
+        }
+
+        if (options.postData !== undefined) {
+            _.extend(data, options.postData);
+        }
+
+        changingState = (options.type !== this.get('state'));
+
+        saveOptions = _.defaults({
+            data: data,
+
+            success: _.bind(function() {
+                if (changingState) {
+                    this.trigger('closed');
+                }
+
+                this.markUpdated(this.get('lastUpdated'));
+
+                if (_.isFunction(options.success)) {
+                    options.success.call(context);
+                }
+            }, this)
+        }, options);
+
+        delete saveOptions.type;
+        delete saveOptions.description;
+
+        this.save(saveOptions, context);
+    },
+
+    /*
+     * Reopens the review request.
+     */
+    reopen: function(options, context) {
+        options = options || {};
+
+        this.save(
+            _.defaults({
+                data: {
+                    status: 'pending'
+                },
+
+                success: _.bind(function() {
+                    this.trigger('reopened');
+                    this.markUpdated(this.get('lastUpdated'));
+
+                    if (_.isFunction(options.success)) {
+                        options.success.call(context);
+                    }
+                }, this)
+            }, options),
+            context);
+    },
+
+    /*
+     * Marks the review request as having been updated at the given timestamp.
+     *
+     * This should be used when an action will trigger an update to the
+     * review request's Last Updated timestamp, but where we don't want
+     * a notification later on. The local copy of the timestamp can be
+     * bumped to mark it as up-to-date.
+     */
+    markUpdated: function(timestamp) {
+        this._lastUpdateTimestamp = timestamp;
+    },
+
+    /*
+     * Begins checking for server-side updates to the review request.
+     *
+     * This takes a type of update to check for, and the last known
+     * updated timestamp.
+     *
+     * The 'updated' event will be triggered when there's a new update.
+     */
+    beginCheckForUpdates: function(type, lastUpdateTimestamp) {
+        this._checkUpdatesType = type;
+        this._lastUpdateTimestamp = lastUpdateTimestamp;
+
+        this.ready({
+            ready: function() {
+                setTimeout(_.bind(this._checkForUpdates, this),
+                           RB.ReviewRequest.CHECK_UPDATES_MSECS);
+            }
+        }, this);
+    },
+
+    /*
+     * Checks for updates.
+     *
+     * This is called periodically after an initial call to
+     * beginCheckForUpdates. It will see if there's a new update yet on the
+     * server, and if there is, trigger the 'updated' event.
+     */
+    _checkForUpdates: function() {
+        RB.apiCall({
+            type: 'GET',
+            prefix: this.get('sitePrefix'),
+            noActivityIndicator: true,
+            url: this.get('links').last_update.href,
+            success: _.bind(function(rsp) {
+                var lastUpdate = rsp.last_update;
+                if ((this._checkUpdatesType === undefined ||
+                     this._checkUpdatesType === lastUpdate.type) &&
+                    this._lastUpdateTimestamp !== lastUpdate.timestamp) {
+                    this.trigger('updated', lastUpdate);
+                }
+
+                this._lastUpdateTimestamp = lastUpdate.timestamp;
+
+                setTimeout(_.bind(this._checkForUpdates, this),
+                           RB.ReviewRequest.CHECK_UPDATES_MSECS);
+            }, this)
+        });
+    },
+
+    /*
+     * Serialize for sending to the server.
+     */
+    toJSON: function(options) {
+        var commitID = this.get('commitID'),
+            repository = this.get('repository'),
+            result = {};
+
+        options = options || {};
+
+        if (this.isNew()) {
+            if (commitID) {
+                result.commit_id = commitID;
+
+                if (options.createFromCommit) {
+                    result.create_from_commit_id = true;
+                }
+            }
+
+            if (repository) {
+                result.repository = repository;
+            }
+
+            return result;
+        } else {
+            return _super(this).toJSON.apply(this, arguments);
+        }
+    },
+
+    /*
+     * Deserialize the response from the server.
+     */
+    parseResourceData: function(rsp) {
+        var state = {
+                pending: RB.ReviewRequest.PENDING,
+                discarded: RB.ReviewRequest.CLOSE_DISCARDED,
+                submitted: RB.ReviewRequest.CLOSE_SUBMITTED
+            }[rsp.status],
+            rawTextFields = rsp.raw_text_fields || rsp,
+            data = RB.BaseResource.prototype.parseResourceData.call(this, rsp);
+
+        data.state = state;
+        data.closeDescriptionRichText =
+            (rawTextFields.close_description_text_type === 'markdown');
+        data.descriptionRichText =
+            (rawTextFields.description_text_type === 'markdown');
+        data.testingDoneRichText =
+            (rawTextFields.testing_done_text_type === 'markdown');
+
+        return data;
+    }
+}, {
+    CHECK_UPDATES_MSECS: 5 * 60 * 1000, // Every 5 minutes
+
+    CLOSE_DISCARDED: 1,
+    CLOSE_SUBMITTED: 2,
+    PENDING: 3,
+
+    VISIBILITY_VISIBLE: 1,
+    VISIBILITY_ARCHIVED: 2,
+    VISIBILITY_MUTED: 3
+});
--- a/reviewboard/reviewboard/staticbundles.py
+++ b/reviewboard/reviewboard/staticbundles.py
@@ -121,32 +121,32 @@ PIPELINE_JS = dict({
             'rb/js/resources/models/baseResourceModel.js',
             'rb/js/resources/models/apiTokenModel.js',
             'rb/js/resources/models/repositoryBranchModel.js',
             'rb/js/resources/models/repositoryCommitModel.js',
             'rb/js/resources/models/draftResourceChildModelMixin.js',
             'rb/js/resources/models/draftResourceModelMixin.js',
             'rb/js/resources/models/draftReviewRequestModel.js',
             'rb/js/resources/models/reviewModel.js',
-            'rb/js/resources/models/draftReviewModel.js',
-            'rb/js/resources/models/baseCommentModel.js',
+            'rb/js/resources/models/draftReviewModel_mozreview.js',
+            'rb/js/resources/models/baseCommentModel_mozreview.js',
             'rb/js/resources/models/baseCommentReplyModel.js',
             'rb/js/resources/models/defaultReviewerModel.js',
-            'rb/js/resources/models/diffCommentModel.js',
+            'rb/js/resources/models/diffCommentModel_mozreview.js',
             'rb/js/resources/models/diffCommentReplyModel.js',
             'rb/js/resources/models/diffModel.js',
             'rb/js/resources/models/fileAttachmentModel.js',
             'rb/js/resources/models/fileAttachmentCommentModel.js',
             'rb/js/resources/models/fileAttachmentCommentReplyModel.js',
             'rb/js/resources/models/fileDiffModel.js',
             'rb/js/resources/models/draftFileAttachmentModel.js',
             'rb/js/resources/models/repositoryModel.js',
             'rb/js/resources/models/reviewGroupModel.js',
             'rb/js/resources/models/reviewReplyModel.js',
-            'rb/js/resources/models/reviewRequestModel.js',
+            'rb/js/resources/models/reviewRequestModel_mozreview.js',
             'rb/js/resources/models/screenshotModel.js',
             'rb/js/resources/models/screenshotCommentModel.js',
             'rb/js/resources/models/screenshotCommentReplyModel.js',
             'rb/js/resources/models/validateDiffModel.js',
             'rb/js/resources/collections/resourceCollection.js',
             'rb/js/resources/collections/repositoryBranchesCollection.js',
             'rb/js/resources/collections/repositoryCommitsCollection.js',
             'rb/js/ui/views/dialogView.js',
@@ -237,17 +237,17 @@ PIPELINE_JS = dict({
             'rb/js/views/textBasedReviewableView.js',
             'rb/js/views/textCommentRowSelector.js',
             'rb/js/views/markdownReviewableView.js',
             'rb/js/views/uploadDiffView.js',
             'rb/js/views/updateDiffView.js',
             'rb/js/diffviewer/models/diffCommentBlockModel.js',
             'rb/js/diffviewer/models/diffCommentsHintModel.js',
             'rb/js/diffviewer/models/diffFileModel.js',
-            'rb/js/diffviewer/models/diffReviewableModel.js',
+            'rb/js/diffviewer/models/diffReviewableModel_mozreview.js',
             'rb/js/diffviewer/models/diffRevisionModel.js',
             'rb/js/diffviewer/models/paginationModel.js',
             'rb/js/diffviewer/collections/diffFileCollection.js',
             'rb/js/diffviewer/views/chunkHighlighterView.js',
             'rb/js/diffviewer/views/diffCommentBlockView.js',
             'rb/js/diffviewer/views/diffCommentsHintView.js',
             'rb/js/diffviewer/views/diffComplexityIconView.js',
             'rb/js/diffviewer/views/diffFileIndexView.js',