← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cprov/launchpad/cric-api-take2 into lp:launchpad

 

Celso Providelo has proposed merging lp:~cprov/launchpad/cric-api-take2 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cprov/launchpad/cric-api-take2/+merge/200563


-- 
https://code.launchpad.net/~cprov/launchpad/cric-api-take2/+merge/200563
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cprov/launchpad/cric-api-take2 into lp:launchpad.
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js	2013-12-12 04:47:30 +0000
+++ lib/lp/app/javascript/client.js	2014-01-06 17:56:12 +0000
@@ -183,8 +183,9 @@
             for (index = 0; index < value.length; index++) {
                 elems.push(enc(key) + "=" + enc(value[index]));
             }
-        }
-        else {
+        } else if (Y.Lang.isObject(value)) {
+            elems.push(enc(key) + "=" + Y.JSON.stringify(value));
+        } else {
             elems.push(enc(key) + "=" + enc(value));
         }
         return elems.join("&");
@@ -1256,5 +1257,5 @@
         }
     });
 }, "0.1", {
-    requires: ["base", "plugin", "dump", "lp.client"]
+    requires: ["base", "plugin", "dump", "json", "lp.client"]
 });

=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py	2013-09-05 03:57:28 +0000
+++ lib/lp/code/browser/branchmergeproposal.py	2014-01-06 17:56:12 +0000
@@ -61,6 +61,7 @@
     SimpleTerm,
     SimpleVocabulary,
     )
+from zope.security.proxy import removeSecurityProxy
 
 from lp import _
 from lp.app.browser.launchpadform import (
@@ -92,6 +93,9 @@
     )
 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
 from lp.code.interfaces.diff import IPreviewDiff
 from lp.registry.interfaces.person import IPersonSet
@@ -627,6 +631,11 @@
                 self.context).event_key
         if getFeatureFlag("code.inline_diff_comments.enabled"):
             cache.objects['inline_diff_comments'] = True
+            cache.objects['preview_diff_timestamps'] = [
+                pd.date_created for pd in self.context.preview_diffs]
+            cache.objects['published_inline_comments'] = (
+                getUtility(ICodeReviewInlineCommentSet).findByPreviewDiff(
+                    removeSecurityProxy(self.context.preview_diff), self.user))
 
     @action('Claim', name='claim')
     def claim_action(self, action, data):
@@ -713,9 +722,7 @@
 
         If no preview is available, try using the review diff.
         """
-        if self.context.preview_diff is not None:
-            return self.context.preview_diff
-        return None
+        return self.context.preview_diff
 
     @property
     def has_bug_or_spec(self):
@@ -1462,8 +1469,7 @@
         else:
             review_type = self.users_vote_ref.review_type
         # We'll be positive here and default the vote to approve.
-        return {'vote': CodeReviewVote.APPROVE,
-                'review_type': review_type}
+        return {'vote': CodeReviewVote.APPROVE, 'review_type': review_type}
 
     def initialize(self):
         """Get the users existing vote reference."""
@@ -1478,10 +1484,10 @@
         if self.user is None:
             # Anonymous users are not valid voters.
             raise AssertionError('Invalid voter')
-        LaunchpadFormView.initialize(self)
+        super(BranchMergeProposalAddVoteView, self).initialize()
 
     def setUpFields(self):
-        LaunchpadFormView.setUpFields(self)
+        super(BranchMergeProposalAddVoteView, self).setUpFields()
         self.reviewer = self.user.name
         # claim_review is set in situations where a user is reviewing on
         # behalf of a team.
@@ -1512,8 +1518,7 @@
         # the data dict.  If this is the case, get the review_type from the
         # hidden field that we so cunningly added to the form.
         review_type = data.get(
-            'review_type',
-            self.request.form.get('review_type'))
+            'review_type', self.request.form.get('review_type'))
         # Translate the request parameter back into what our object model
         # needs.
         if review_type == '':

=== modified file 'lib/lp/code/browser/codereviewcomment.py'
--- lib/lp/code/browser/codereviewcomment.py	2013-04-11 04:54:04 +0000
+++ lib/lp/code/browser/codereviewcomment.py	2014-01-06 17:56:12 +0000
@@ -13,6 +13,7 @@
 
 from lazr.delegates import delegates
 from lazr.restful.interface import copy_field
+from zope.component import getUtility
 from zope.formlib.widgets import (
     DropdownWidget,
     TextAreaWidget,
@@ -21,7 +22,10 @@
     implements,
     Interface,
     )
-from zope.schema import Text
+from zope.schema import (
+    Bool,
+    Text,
+    )
 
 from lp import _
 from lp.app.browser.launchpadform import (
@@ -30,11 +34,15 @@
     LaunchpadFormView,
     )
 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
 from lp.services.comments.browser.comment import download_body
 from lp.services.comments.browser.messagecomment import MessageComment
 from lp.services.comments.interfaces.conversation import IComment
 from lp.services.config import config
+from lp.services.features import getFeatureFlag
 from lp.services.librarian.interfaces import ILibraryFileAlias
 from lp.services.propertycache import (
     cachedproperty,
@@ -214,6 +222,9 @@
 
     comment = Text(title=_('Comment'), required=False)
 
+    publish_inline_comments = Bool(
+        title=_("Publish draft inline comments"), required=False)
+
 
 class CodeReviewCommentAddView(LaunchpadFormView):
     """View for adding a CodeReviewComment."""
@@ -242,6 +253,11 @@
             comment = ''
         return {'comment': comment}
 
+    def setUpFields(self):
+        super(CodeReviewCommentAddView, self).setUpFields()
+        if not getFeatureFlag('code.inline_diff_comments.enabled'):
+            self.form_fields.omit('publish_inline_comments')
+
     @property
     def is_reply(self):
         """True if this comment is a reply to another comment, else False."""
@@ -268,9 +284,14 @@
         """Create the comment..."""
         vote = data.get('vote')
         review_type = data.get('review_type')
-        self.branch_merge_proposal.createComment(
+        comment = self.branch_merge_proposal.createComment(
             self.user, subject=None, content=data['comment'],
             parent=self.reply_to, vote=vote, review_type=review_type)
+        if (
+            getFeatureFlag('code.inline_diff_comments.enabled') and
+            data.get('publish_inline_comments')):
+            getUtility(ICodeReviewInlineCommentSet).publishDraft(
+                self.branch_merge_proposal.preview_diff, self.user, comment)
 
     @property
     def next_url(self):

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2013-11-21 03:42:38 +0000
+++ lib/lp/code/configure.zcml	2014-01-06 17:56:12 +0000
@@ -586,6 +586,23 @@
       provides="lp.services.webapp.interfaces.IPrimaryContext"
       factory="lp.code.browser.codereviewcomment.CodeReviewCommentPrimaryContext"/>
 
+  <!-- CodeReviewInlineComment -->
+
+  <class class="lp.code.model.codereviewinlinecomment.CodeReviewInlineComment">
+    <allow interface="lp.code.interfaces.codereviewinlinecomment.ICodeReviewInlineComment"/>
+  </class>
+
+  <!-- CodeReviewInlineCommentSet -->
+
+  <securedutility
+      class="lp.code.model.codereviewinlinecomment.CodeReviewInlineCommentSet"
+      provides="lp.code.interfaces.codereviewinlinecomment.ICodeReviewInlineCommentSet">
+    <allow interface="lp.code.interfaces.codereviewinlinecomment.ICodeReviewInlineCommentSet"/>
+  </securedutility>
+  <class class="lp.code.model.codereviewinlinecomment.CodeReviewInlineCommentSet">
+    <allow interface="lp.code.interfaces.codereviewinlinecomment.ICodeReviewInlineCommentSet"/>
+  </class>
+
   <!-- hierarchy -->
 
   <class class="lp.code.model.branchjob.BranchJob">

=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2012-08-28 00:07:47 +0000
+++ lib/lp/code/errors.py	2014-01-06 17:56:12 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Errors used in the lp/code modules."""
@@ -27,6 +27,7 @@
     'CodeImportAlreadyRunning',
     'CodeImportNotInReviewedState',
     'ClaimReviewFailed',
+    'DiffNotFound',
     'InvalidBranchMergeProposal',
     'InvalidMergeQueueConfig',
     'InvalidNamespace',
@@ -384,3 +385,8 @@
     def __init__(self):
         message = ('The configuration specified is not a valid JSON string.')
         Exception.__init__(self, message)
+
+
+@error_status(httplib.BAD_REQUEST)
+class DiffNotFound(Exception):
+    """A `IPreviewDiff` with the timestamp was not found."""

=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py	2013-12-16 05:04:43 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py	2014-01-06 17:56:12 +0000
@@ -56,6 +56,7 @@
     Bool,
     Choice,
     Datetime,
+    Dict,
     Int,
     Object,
     Text,
@@ -569,6 +570,20 @@
         :param original_email: Original email message.
         """
 
+    @export_write_operation()
+    @operation_parameters(
+        diff_timestamp=Datetime(), comments=Dict(key_type=TextLine()))
+    @call_with(person=REQUEST_USER)
+    @operation_for_version('devel')
+    def saveDraftInlineComment(diff_timestamp, person, comments):
+        """Save `ICodeReviewInlineCommentDraft`
+
+        :param diff_timestamp: The timestamp of the `PreviewDiff` the comments
+            are on.
+        :param person: The `IPerson` making the comments.
+        :param comments: The comments.
+        """
+
 
 class IBranchMergeProposal(IBranchMergeProposalPublic,
                            IBranchMergeProposalView, IBranchMergeProposalEdit,

=== added file 'lib/lp/code/interfaces/codereviewinlinecomment.py'
--- lib/lp/code/interfaces/codereviewinlinecomment.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/codereviewinlinecomment.py	2014-01-06 17:56:12 +0000
@@ -0,0 +1,67 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for inline comments with preview diffs."""
+
+__metaclass__ = type
+__all__ = [
+    'ICodeReviewInlineComment',
+    'ICodeReviewInlineCommentSet',
+    ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+    Datetime,
+    TextLine,
+    )
+
+from lp import _
+from lp.code.interfaces.codereviewcomment import ICodeReviewComment
+from lp.code.interfaces.diff import IPreviewDiff
+from lp.registry.interfaces.person import IPerson
+
+
+class ICodeReviewInlineComment(Interface):
+    previewdiff = Reference(
+        title=_('The preview diff'), schema=IPreviewDiff, required=True,
+        readonly=True)
+    person = Reference(
+        title=_('Person'), schema=IPerson, required=True, readonly=True)
+    comment = Reference(
+        title=_('The branch merge proposal comment'),
+        schema=ICodeReviewComment, required=True, readonly=True)
+    date_created = Datetime(
+        title=_('The date on which the comments were published'),
+        required=True, readonly=True)
+    comments = TextLine(
+        title=_('Inline comments'), required=True, readonly=True)
+
+
+class ICodeReviewInlineCommentSet(Interface):
+    def findDraft(previewdiff, person):
+        """Find the draft comments for a given diff and person.
+
+        :param previewdiff: The `PreviewDiff` these comments are for.
+        :param person: The `Person` making the comments.
+        """
+
+    def ensureDraft(previewdiff, person, comments):
+        """Ensure a `ICodeReviewInlineCommentDraft` is up to date. This method
+        will also delete an existing draft if the comments are empty.
+
+        :param previewdiff: The `PreviewDiff` these comments are for.
+        :param person: The `Person` making the comments.
+        :param comments: The comments themselves.
+        """
+
+    def publishDraft(previewdiff, person, comment):
+        """Publish code review inline comments so other people can view them.
+
+        :param previewdiff: The `PreviewDiff` these comments are for.
+        :param person: The `Person` making the comments.
+        :param comment: The `CodeReviewComment` linked to the comments.
+        """
+
+    def findByPreviewDiff(previewdiff):
+        """Find all comments for a given `PreviewDiff`."""

=== modified file 'lib/lp/code/javascript/branchmergeproposal.inlinecomments.js'
--- lib/lp/code/javascript/branchmergeproposal.inlinecomments.js	2013-09-17 06:14:49 +0000
+++ lib/lp/code/javascript/branchmergeproposal.inlinecomments.js	2014-01-06 17:56:12 +0000
@@ -11,9 +11,9 @@
 
 // Grab the namespace in order to be able to expose the connect methods.
 var namespace = Y.namespace('lp.code.branchmergeproposal.inlinecomments');
+var inlinecomments = {};
 
 namespace.add_doubleclick_handler = function() {
-    var inlinecomments = {};
     var rows = Y.one('.diff').all('tr');
     var handling_request = false;
     handler = function(e) {
@@ -39,9 +39,20 @@
                 inlinecomments[rownumber] = comment;
             }
             if (comment === '') {
+                delete inlinecomments[rownumber];
                 headerrow.remove(true);
                 newrow.remove(true);
             }
+            var diff_timestamp = LP.cache.preview_diff_timestamps.slice(-1)[0];
+            var config = {
+                parameters: {
+                    diff_timestamp: diff_timestamp,
+                    comments: inlinecomments
+                }
+            };
+            lp_client = new Y.lp.client.Launchpad();
+            lp_client.named_post(
+                LP.cache.context.self_link, 'saveDraftInlineComment', config);
             handling_request = false;
         };
         widget.editor.on('save', function() {
@@ -90,6 +101,10 @@
             '<div class="yui3-editable_text-trigger"></div>' +
             '</div></td></tr>').set('id', 'ict-' + middle);
         newrow.one('td>div').set('id', 'inlinecomment-' + middle);
+        if (comment !== null) {
+            inlinecomments[rownumber] = comment;
+            //newrow.one('span').set('text', comment);
+        }
     } else {
         newrow = Y.Node.create('<tr><td></td><td><span></span></td></tr>');
         newrow.one('span').set('text', comment);

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2013-06-20 05:50:00 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2014-01-06 17:56:12 +0000
@@ -45,6 +45,7 @@
     BadBranchMergeProposalSearchContext,
     BadStateTransition,
     BranchMergeProposalExists,
+    DiffNotFound,
     UserNotBranchReviewer,
     WrongBranchMergeProposal,
     )
@@ -63,6 +64,9 @@
     )
 from lp.code.interfaces.branchrevision import IBranchRevision
 from lp.code.interfaces.branchtarget import IHasBranchTarget
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
 from lp.code.mail.branch import RecipientReason
 from lp.code.model.branchrevision import BranchRevision
 from lp.code.model.codereviewcomment import CodeReviewComment
@@ -869,6 +873,19 @@
                     code_review_message, original_email))
         return code_review_message
 
+    def saveDraftInlineComment(self, diff_timestamp, person, comments):
+        """See `IBranchMergeProposal`."""
+        previewdiff = IStore(PreviewDiff).find(
+            PreviewDiff,
+            PreviewDiff.branch_merge_proposal_id == self.id,
+            PreviewDiff.date_created == diff_timestamp).one()
+        if not previewdiff:
+            raise DiffNotFound(
+                "Could not locate a preview diff with a timestamp of %s" % (
+                    diff_timestamp))
+        getUtility(ICodeReviewInlineCommentSet).ensureDraft(
+            previewdiff, person, comments)
+
     def updatePreviewDiff(self, diff_content, source_revision_id,
                           target_revision_id, prerequisite_revision_id=None,
                           conflicts=None):

=== added file 'lib/lp/code/model/codereviewinlinecomment.py'
--- lib/lp/code/model/codereviewinlinecomment.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/codereviewinlinecomment.py	2014-01-06 17:56:12 +0000
@@ -0,0 +1,123 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Model classes for inline comments for preview diffs."""
+
+__metaclass__ = type
+__all__ = [
+    'CodeReviewInlineComment',
+    'CodeReviewInlineCommentDraft',
+    'CodeReviewInlineCommentSet',
+    ]
+
+import simplejson
+from storm.locals import (
+    Int,
+    Reference,
+    Unicode,
+    )
+from zope.component import getUtility
+from zope.interface import implements
+
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineComment,
+    ICodeReviewInlineCommentSet,
+    )
+from lp.code.model.codereviewcomment import CodeReviewComment
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.database.bulk import load_related
+from lp.services.database.interfaces import IStore
+from lp.services.database.stormbase import StormBase
+
+
+class CodeReviewInlineComment(StormBase):
+    __storm_table__ = 'CodeReviewInlineComment'
+
+    implements(ICodeReviewInlineComment)
+
+    previewdiff_id = Int(name='previewdiff')
+    previewdiff = Reference(previewdiff_id, 'PreviewDiff.id')
+    person_id = Int(name='person')
+    person = Reference(person_id, 'Person.id')
+    comment_id = Int(name='comment', primary=True)
+    comment = Reference(comment_id, 'CodeReviewComment.id')
+    comments = Unicode()
+
+
+class CodeReviewInlineCommentDraft(StormBase):
+    __storm_table__ = 'CodeReviewInlineCommentDraft'
+    __storm_primary__ = ('previewdiff_id', 'person_id')
+
+    previewdiff_id = Int(name='previewdiff')
+    previewdiff = Reference(previewdiff_id, 'PreviewDiff.id')
+    person_id = Int(name='person')
+    person = Reference(person_id, 'Person.id')
+    comments = Unicode()
+
+
+class CodeReviewInlineCommentSet:
+
+    implements(ICodeReviewInlineCommentSet)
+
+    def _findDraftObject(self, previewdiff, person):
+        return IStore(CodeReviewInlineCommentDraft).find(
+            CodeReviewInlineCommentDraft,
+            CodeReviewInlineCommentDraft.previewdiff_id == previewdiff.id,
+            CodeReviewInlineCommentDraft.person_id == person.id).one()
+
+    def findDraft(self, previewdiff, person):
+        cricd = self._findDraftObject(previewdiff, person)
+        if cricd:
+            return simplejson.loads(cricd.comments)
+        return {}
+
+    def ensureDraft(self, previewdiff, person, comments):
+        cricd = self._findDraftObject(previewdiff, person)
+        if not comments:
+            if cricd:
+                IStore(CodeReviewInlineCommentDraft).remove(cricd)
+        else:
+            if type(comments) == dict:
+                comments = simplejson.dumps(comments).decode('utf-8')
+            if cricd:
+                cricd.comments = comments
+            else:
+                cricd = CodeReviewInlineCommentDraft()
+                cricd.previewdiff = previewdiff
+                cricd.person = person
+                cricd.comments = comments
+                IStore(CodeReviewInlineCommentDraft).add(cricd)
+
+    def publishDraft(self, previewdiff, person, comment):
+        cricd = self._findDraftObject(previewdiff, person)
+        assert cricd is not None
+        cric = CodeReviewInlineComment()
+        cric.previewdiff = cricd.previewdiff
+        cric.person = cricd.person
+        cric.comment = comment
+        cric.comments = cricd.comments
+        IStore(CodeReviewInlineComment).add(cric)
+        IStore(CodeReviewInlineComment).remove(cricd)
+        return cric
+
+    def findByPreviewDiff(self, previewdiff, person):
+        crics = IStore(CodeReviewInlineComment).find(
+            CodeReviewInlineComment,
+            CodeReviewInlineComment.previewdiff_id == previewdiff.id)
+        getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+            [cric.person_id for cric in crics])
+        load_related(CodeReviewComment, crics, ['comment_id'])
+        sorted_crics = sorted(
+            list(crics), key=lambda c: c.comment.date_created)
+        inline_comments = []
+        for cric in sorted_crics:
+            comments = simplejson.loads(cric.comments)
+            for lineno in comments:
+                inline_comments.append(
+                    [lineno, cric.person, comments[lineno],
+                        cric.comment.date_created])
+        draft_comments = self.findDraft(previewdiff, person)
+        for draft_lineno in draft_comments:
+            inline_comments.append(
+                [draft_lineno, None, draft_comments[draft_lineno], None])
+        return inline_comments

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2013-05-14 14:30:09 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2014-01-06 17:56:12 +0000
@@ -47,6 +47,9 @@
     IBranchMergeProposalGetter,
     notify_modified,
     )
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
 from lp.code.model.branchmergeproposal import (
     BranchMergeProposalGetter,
     is_valid_transition,
@@ -2087,3 +2090,22 @@
         user = previewdiff.branch_merge_proposal.target_branch.owner
         ws_previewdiff = self.wsObject(previewdiff, user=user)
         self.assertIsNone(ws_previewdiff.diffstat)
+
+    def test_saveDraftInlineComment_with_no_previewdiff(self):
+        bmp = self.factory.makeBranchMergeProposal()
+        ws_bmp = self.wsObject(bmp, user=bmp.target_branch.owner)
+        self.assertRaises(
+            BadRequest, ws_bmp.saveDraftInlineComment,
+            diff_timestamp=datetime(2001, 1, 1, 12, tzinfo=UTC), comments={})
+
+    def test_saveDraftInlineComment(self):
+        previewdiff = self.factory.makePreviewDiff()
+        user = previewdiff.branch_merge_proposal.target_branch.owner
+        ws_bmp = self.wsObject(previewdiff.branch_merge_proposal, user=user)
+        comments = {u'2': u'foo'}
+        ws_bmp.saveDraftInlineComment(
+            diff_timestamp=previewdiff.date_created, comments=comments)
+        transaction.commit()
+        draft_comments = getUtility(ICodeReviewInlineCommentSet).findDraft(
+            previewdiff, user)
+        self.assertEqual(comments, draft_comments)

=== added file 'lib/lp/code/model/tests/test_codereviewinlinecomment.py'
--- lib/lp/code/model/tests/test_codereviewinlinecomment.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_codereviewinlinecomment.py	2014-01-06 17:56:12 +0000
@@ -0,0 +1,100 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for CodeReviewInlineComment{,Draft,Set}"""
+
+__metaclass__ = type
+
+from datetime import datetime
+
+from pytz import UTC
+from zope.component import getUtility
+
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import LaunchpadFunctionalLayer
+
+
+class TestCodeReviewInlineComment(TestCaseWithFactory):
+    layer = LaunchpadFunctionalLayer
+
+    def _makeCRICD(self):
+        previewdiff = self.factory.makePreviewDiff()
+        person = self.factory.makePerson()
+        self.factory.makeCodeReviewInlineCommentDraft(
+            previewdiff=previewdiff, person=person)
+        return previewdiff, person
+
+    def test_ensure_creates(self):
+        # ICodeReviewInlineCommentSet.ensureDraft() will create one if it
+        # does not exist.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff, person = self._makeCRICD()
+        self.assertEqual(
+            {'2': 'foobar'}, interface.findDraft(previewdiff, person))
+
+    def test_ensure_deletes(self):
+        # ICodeReviewInlineCommentSet.ensureDraft() will delete a draft if
+        # no comments are provided.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff, person = self._makeCRICD()
+        interface.ensureDraft(previewdiff, person, {})
+        self.assertEqual({}, interface.findDraft(previewdiff, person))
+
+    def test_ensure_deletes_with_no_draft(self):
+        # ICodeReviewInlineCommentSet.ensureDraft() will cope with a draft
+        # that does not exist when called with no comments.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff = self.factory.makePreviewDiff()
+        person = self.factory.makePerson()
+        interface.ensureDraft(previewdiff, person, {})
+        self.assertEqual({}, interface.findDraft(previewdiff, person))
+
+    def test_ensure_updates(self):
+        # ICodeReviewInlineCommentSet.ensureDraft() will update the draft when
+        # the comments change.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff, person = self._makeCRICD()
+        interface.ensureDraft(previewdiff, person, {'1': 'bar'})
+        comment = interface.findDraft(previewdiff, person)
+        self.assertEqual({'1': 'bar'}, comment)
+
+    def test_publishDraft(self):
+        # ICodeReviewInlineCommentSet.publishDraft() will publish draft
+        # comments.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff, person = self._makeCRICD()
+        comment = self.factory.makeCodeReviewComment(
+            merge_proposal=previewdiff.branch_merge_proposal, sender=person)
+        cric = interface.publishDraft(previewdiff, person, comment)
+        self.assertIsNot(None, cric)
+        self.assertEqual({}, interface.findDraft(previewdiff, person))
+
+    def test_findByPreviewDiff_draft(self):
+        # ICodeReviewInlineCommentSet.findByPreviewDiff() will return a draft
+        # so it can be rendered.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff, person = self._makeCRICD()
+        results = interface.findByPreviewDiff(previewdiff, person) 
+        self.assertEqual([[u'2', None, u'foobar', None]], results)
+
+    def test_findByPreviewDiff_sorted(self):
+        # ICodeReviewInlineCommentSet.findByPreviewDiff() will return a sorted
+        # list.
+        interface = getUtility(ICodeReviewInlineCommentSet)
+        previewdiff = self.factory.makePreviewDiff()
+        person = self.factory.makePerson()
+        comment = self.factory.makeCodeReviewComment()
+        self.factory.makeCodeReviewInlineComment(
+            previewdiff=previewdiff, person=person, comment=comment)
+        old_comment = self.factory.makeCodeReviewComment(
+            date_created=datetime(2001, 1, 1, 12, tzinfo=UTC))
+        self.factory.makeCodeReviewInlineComment(
+            previewdiff=previewdiff, person=person, comment=old_comment,
+            comments={'8': 'baz'})
+        self.assertEqual(
+            [[u'8', person, u'baz', old_comment.date_created],
+             [u'2', person, u'foobar', comment.date_created]],
+            interface.findByPreviewDiff(previewdiff, person))

=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
--- lib/lp/code/templates/branchmergeproposal-index.pt	2013-09-05 03:57:28 +0000
+++ lib/lp/code/templates/branchmergeproposal-index.pt	2014-01-06 17:56:12 +0000
@@ -144,6 +144,7 @@
         <div id="add-comment-review-fields">
             Review: <tal:review replace="structure comment_form/widgets/vote"/>
             Review type: <tal:review replace="structure comment_form/widgets/review_type"/>
+            <tal:publish_inline_comments condition="features/code.inline_diff_comments.enabled">Publish inline comments: <tal:review replace="structure comment_form/widgets/publish_inline_comments" tal:condition="features/code.inline_diff_comments.enabled"/></tal:publish_inline_comments>
             <div class="actions"
                  tal:content="structure comment_form/actions/field.actions.add/render" />
         </div>

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2013-11-30 00:12:15 +0000
+++ lib/lp/testing/factory.py	2014-01-06 17:56:12 +0000
@@ -116,6 +116,9 @@
 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
+from lp.code.interfaces.codereviewinlinecomment import (
+    ICodeReviewInlineCommentSet,
+    )
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.interfaces.sourcepackagerecipe import (
@@ -4316,6 +4319,27 @@
         product.redeemSubscriptionVoucher(
             self.getUniqueString(), person, person, months)
 
+    def makeCodeReviewInlineCommentDraft(self, previewdiff=None, person=None,
+                                         comments={'2': 'foobar'}):
+        if previewdiff is None:
+            previewdiff = self.makePreviewDiff()
+        if person is None:
+            person = self.makePerson()
+        getUtility(ICodeReviewInlineCommentSet).ensureDraft(
+            previewdiff, person, comments)
+
+    def makeCodeReviewInlineComment(self, previewdiff=None, person=None,
+                                    comment=None, comments={'2': 'foobar'}):
+        if previewdiff is None:
+            previewdiff = self.makePreviewDiff()
+        if person is None:
+            person = self.makePerson()
+        if comment is None:
+            comment = self.makeCodeReviewComment()
+        self.makeCodeReviewInlineCommentDraft(previewdiff, person, comments)
+        return getUtility(ICodeReviewInlineCommentSet).publishDraft(
+            previewdiff, person, comment)
+
 
 # Some factory methods return simple Python types. We don't add
 # security wrappers for them, as well as for objects created by


Follow ups