← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/bugcomment-as-icomment into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/bugcomment-as-icomment into lp:launchpad with lp:~abentley/launchpad/comment-lint as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/bugcomment-as-icomment/+merge/89497

= Summary =
Truncate long code review comments and provide "Read more..." link.

== Proposed fix ==
Use common rendering for BugComment and CodeReviewComment

== Pre-implementation notes ==
Discussed with deryck

== Implementation details ==
Provide MessageComment to implement IComment in terms of IMessage, and use in CodeReivewComment, BugComment, DistroSeriesDifferenceDisplayComment.  Provide adapters to convert CodeReviewComment and DistroSeriesDifferenceDisplayComment into IMessage.

Move the code for truncation/Read More from bug comments to lp.services.comments.

IComment gains too_long and text_for_display attributes.  MessageComment implements them.

Support truncation to 3200 chars in CodeReviewDisplayComment.

Adjust CodeReviewDisplayComment call sites to specify whether to truncate.

Use a single "comment-text" css class, instead of multiple ones.

Add browser_open to harness.py, to simplify interactive testing.

== Tests ==
bin/test -t test_bugcomment -t bugcomment.txt -t xx-bug-comments-truncated.txt -t xx-bug-index-lots-of-comments.txt -t xx-numbered-comments.txt -t test_branchmergeproposal -t test_codereviewcomment -t conversation.txt

== Demo and Q/A ==
Create a code review comment longer than 3200 chars.  It should be truncated and have a "Read more" link.  That link should display the full code review comment.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/services/comments/browser/configure.zcml
  lib/lp/registry/browser/configure.zcml
  lib/lp/bugs/browser/tests/test_bugcomment.py
  lib/lp/code/browser/configure.zcml
  lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt
  lib/lp/code/browser/branch.py
  lib/lp/code/templates/codereviewcomment-body.pt
  lib/canonical/launchpad/icing/css/modifiers.css
  lib/lp/code/browser/tests/test_branchmergeproposal.py
  lib/lp/code/browser/tests/test_codereviewcomment.py
  lib/lp/bugs/browser/bugcomment.py
  lib/lp/services/comments/browser/messagecomment.py
  lib/lp/registry/browser/distroseriesdifference.py
  lib/lp/testing/factory.py
  lib/lp/services/comments/interfaces/conversation.py
  lib/lp/code/browser/codereviewcomment.py
  lib/lp/bugs/interfaces/bugmessage.py
  lib/lp/scripts/harness.py
  lib/lp/bugs/stories/bugs/xx-numbered-comments.txt
  lib/lp/services/comments/templates/comment-body.pt
  lib/lp/bugs/doc/bugcomment.txt
  lib/lp/services/comments/doc/conversation.txt
  lib/lp/code/browser/branchmergeproposal.py
  lib/lp/bugs/templates/bugcomment-box.pt
  lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt

./lib/canonical/launchpad/icing/css/modifiers.css
      61: Unknown Property name.: filter
      62: Unknown Property name.: -ms-filter
     140: Unknown Property name.: filter
     141: Unknown Property name.: -ms-filter
      61: I009: Wrong separator on property: value pair.
     140: I009: Wrong separator on property: value pair.
./lib/lp/scripts/harness.py
      26: 'from storm.expr import *' used; unable to detect undefined names
      28: 'from storm.locals import *' used; unable to detect undefined names
      21: 'rlcompleter' imported but unused
-- 
https://code.launchpad.net/~abentley/launchpad/bugcomment-as-icomment/+merge/89497
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/bugcomment-as-icomment into lp:launchpad.
=== modified file 'lib/canonical/launchpad/icing/css/modifiers.css'
--- lib/canonical/launchpad/icing/css/modifiers.css	2012-01-20 21:22:32 +0000
+++ lib/canonical/launchpad/icing/css/modifiers.css	2012-01-20 21:22:32 +0000
@@ -155,9 +155,8 @@
 
 pre.changelog,
 table.diff,
-.bug-comment,
-.bug-activity,
-.codereviewcomment {
+.comment-text,
+.bug-activity {
     font-family: 'Ubuntu Mono', monospace;
     }
 

=== modified file 'lib/lp/bugs/browser/bugcomment.py'
--- lib/lp/bugs/browser/bugcomment.py	2012-01-06 19:45:52 +0000
+++ lib/lp/bugs/browser/bugcomment.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2006-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Bug comment browser view classes."""
@@ -41,6 +41,7 @@
 
 from lp.bugs.interfaces.bugattachment import BugAttachmentType
 from lp.bugs.interfaces.bugmessage import IBugComment
+from lp.services.comments.browser.messagecomment import MessageComment
 from lp.services.config import config
 from lp.services.features import getFeatureFlag
 from lp.services.librarian.browser import ProxiedLibraryFileAlias
@@ -176,7 +177,7 @@
             yield [event for (kind, event) in window_group]
 
 
-class BugComment:
+class BugComment(MessageComment):
     """Data structure that holds all data pertaining to a bug comment.
 
     It keeps track of which index it has in the bug comment list and
@@ -194,6 +195,11 @@
     def __init__(
             self, index, message, bugtask, activity=None,
             show_spam_controls=False, user=None, display='full'):
+        if display == 'truncate':
+            comment_limit = config.malone.max_comment_size
+        else:
+            comment_limit = None
+        super(BugComment, self).__init__(comment_limit)
 
         self.index = index
         self.bugtask = bugtask
@@ -216,10 +222,6 @@
         if bool(getFeatureFlag(flag)):
             user_owns_comment = user is not None and user == self.owner
         self.show_spam_controls = show_spam_controls or user_owns_comment
-        if display == 'truncate':
-            self.comment_limit = config.malone.max_comment_size
-        else:
-            self.comment_limit = None
         self.hide_text = (display == 'hide')
 
     @cachedproperty
@@ -239,28 +241,12 @@
         """
         return not self.visible
 
-    @property
-    def needs_truncation(self):
-        if self.comment_limit is None:
-            return False
-        return len(self.text_contents) > self.comment_limit
-
-    @property
-    def was_truncated(self):
-        return self.needs_truncation
-
     @cachedproperty
     def text_for_display(self):
         if self.hide_text:
             return ''
-        if not self.needs_truncation:
-            return self.text_contents
-        # Note here that we truncate at comment_limit, and not
-        # comment_limit - 3; while it would be nice to account for
-        # the ellipsis, this breaks down when the comment limit is
-        # less than 3 (which can happen in a testcase) and it makes
-        # counting the strings harder.
-        return "%s..." % self.text_contents[:self.comment_limit]
+        else:
+            return super(BugComment, self).text_for_display
 
     def isIdenticalTo(self, other):
         """Compare this BugComment to another and return True if they are

=== modified file 'lib/lp/bugs/browser/tests/test_bugcomment.py'
--- lib/lp/bugs/browser/tests/test_bugcomment.py	2012-01-06 19:45:52 +0000
+++ lib/lp/bugs/browser/tests/test_bugcomment.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the bugcomment module."""

=== modified file 'lib/lp/bugs/doc/bugcomment.txt'
--- lib/lp/bugs/doc/bugcomment.txt	2012-01-20 21:22:32 +0000
+++ lib/lp/bugs/doc/bugcomment.txt	2012-01-20 21:22:32 +0000
@@ -115,7 +115,7 @@
 If we get the bug comments from the view we can see that the two additional
 comments have been truncated:
 
-    >>> [(bug_comment.index, bug_comment.was_truncated)
+    >>> [(bug_comment.index, bug_comment.too_long)
     ...  for bug_comment in bug_comments(bug_view)]
     [(1, True), (2, True)]
 

=== modified file 'lib/lp/bugs/interfaces/bugmessage.py'
--- lib/lp/bugs/interfaces/bugmessage.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/interfaces/bugmessage.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2004-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -32,6 +32,7 @@
 from lp.bugs.interfaces.hasbug import IHasBug
 from lp.registry.interfaces.person import IPerson
 from lp.services.fields import Title
+from lp.services.comments.interfaces.conversation import IComment
 from lp.services.messages.interfaces.message import IMessage
 
 
@@ -113,7 +114,7 @@
         required=False, default=None)
 
 
-class IBugComment(IMessage):
+class IBugComment(IMessage, IComment):
     """A bug comment for displaying in the web UI."""
 
     bugtask = Attribute(
@@ -127,11 +128,6 @@
         title=u'A hidden comment still displayed for admins.',
         readonly=True)
     index = Int(title=u'The comment number', required=True, readonly=True)
-    was_truncated = Bool(
-        title=u'Whether the displayed text was truncated for display.',
-        readonly=True)
-    text_for_display = Text(
-        title=u'The comment text to be displayed in the UI.', readonly=True)
     display_title = Attribute('Whether or not to show the title.')
     synchronized = Attribute(
         'Has the comment been synchronized with a remote bug tracker?')

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2012-01-20 21:22:32 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2012-01-20 21:22:32 +0000
@@ -32,14 +32,14 @@
     ...         print div_tag
     >>> print_comments(browser.contents) #doctest: -ELLIPSIS
     <div class="boardCommentBody">
-    <div class="bug-comment" itemprop="commentText"><p>This
+    <div class="comment-text" itemprop="commentText"><p>This
     would be a real killer feature. If there...</p></div>
     <p>
     <a href="/tomcat/+bug/2/comments/1">Read more...</a>
     </p>
     </div>
     <div class="boardCommentBody">
-    <div class="bug-comment" itemprop="commentText"><p>Oddly
+    <div class="comment-text" itemprop="commentText"><p>Oddly
     enough the bug system seems only capabl...</p></div>
     <p>
     <a href="/tomcat/+bug/2/comments/2">Read more...</a>
@@ -59,7 +59,7 @@
 
     >>> print_comments(browser.contents) #doctest: -ELLIPSIS
     <div class="boardCommentBody">
-    <div class="bug-comment" itemprop="commentText"><p>This
+    <div class="comment-text" itemprop="commentText"><p>This
       would be a real killer feature. If there is already code
       to make it possible, why aren't there tons of press announcements
       about the secuirty possibilities. Imagine - no more embarrassing

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt	2011-12-29 05:29:36 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt	2012-01-20 21:22:32 +0000
@@ -8,7 +8,7 @@
 Bug 11 has quite a few comments.
 
     >>> user_browser.open('http://launchpad.dev/bugs/11')
-    >>> comments = find_tags_by_class(user_browser.contents, 'bug-comment')
+    >>> comments = find_tags_by_class(user_browser.contents, 'comment-text')
     >>> len(comments)
     6
 
@@ -26,7 +26,7 @@
 Now only 3 comments will be displayed; the oldest and the 2 newest.
 
     >>> user_browser.open('http://launchpad.dev/bugs/11')
-    >>> comments = find_tags_by_class(user_browser.contents, 'bug-comment')
+    >>> comments = find_tags_by_class(user_browser.contents, 'comment-text')
     >>> len(comments)
     3
 
@@ -59,7 +59,7 @@
     'http://.../jokosher/+bug/11?comments=all'
 
     >>> user_browser.getLink('View all 6 comments').click()
-    >>> comments = find_tags_by_class(user_browser.contents, 'bug-comment')
+    >>> comments = find_tags_by_class(user_browser.contents, 'comment-text')
     >>> len(comments)
     6
 

=== modified file 'lib/lp/bugs/stories/bugs/xx-numbered-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-numbered-comments.txt	2011-10-20 00:53:01 +0000
+++ lib/lp/bugs/stories/bugs/xx-numbered-comments.txt	2012-01-20 21:22:32 +0000
@@ -13,7 +13,7 @@
     ...     number_node = comment.find(None, 'bug-comment-index')
     ...     person_node = comment.find(
     ...         lambda node: 'person' in node.get('class', ''))
-    ...     comment_node = comment.find(None, 'bug-comment')
+    ...     comment_node = comment.find(None, 'comment-text')
     ...     print "%s: %s\n  %s" % (
     ...         extract_text(number_node),
     ...         extract_text(person_node),

=== modified file 'lib/lp/bugs/templates/bugcomment-box.pt'
--- lib/lp/bugs/templates/bugcomment-box.pt	2012-01-11 10:07:56 +0000
+++ lib/lp/bugs/templates/bugcomment-box.pt	2012-01-20 21:22:32 +0000
@@ -90,14 +90,8 @@
       </li>
     </ul>
 
-    <div class="bug-comment" itemprop="commentText"
-      tal:content="structure
-        comment/text_for_display/fmt:obfuscate-email/fmt:email-to-html">
-      Comment text.
-    </div>
-    <p tal:condition="comment/was_truncated">
-      <a tal:attributes="href comment/fmt:url">Read more...</a>
-    </p>
+    <tal:text
+      replace="structure comment/@@+comment-body-text" />
   </div>
 
   <div class="boardCommentFooter" tal:condition="comment/show_footer">

=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py	2011-12-30 06:14:56 +0000
+++ lib/lp/code/browser/branch.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Branch views."""
@@ -1314,7 +1314,7 @@
     for_input = True
 
     custom_widget('target_branch', TargetBranchWidget)
-    custom_widget('comment', TextAreaWidget, cssClass='codereviewcomment')
+    custom_widget('comment', TextAreaWidget, cssClass='comment-text')
 
     page_title = label = 'Propose branch for merging'
 

=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/branchmergeproposal.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=C0322,F0401
@@ -573,6 +573,8 @@
         self.extra_css_class = None
         self.comment_author = None
         self.body_text = None
+        self.text_for_display = None
+        self.too_long = False
         self.comment_date = None
         self.display_attachments = False
 
@@ -648,7 +650,8 @@
         while merge_proposal is not None:
             from_superseded = merge_proposal != self.context
             comments.extend(
-                CodeReviewDisplayComment(comment, from_superseded)
+                CodeReviewDisplayComment(
+                    comment, from_superseded, limit_length=True)
                 for comment in merge_proposal.all_comments)
             merge_proposal = merge_proposal.supersedes
         comments = sorted(comments, key=operator.attrgetter('date'))
@@ -1426,7 +1429,7 @@
     schema = IAddVote
     field_names = ['vote', 'review_type', 'comment']
 
-    custom_widget('comment', TextAreaWidget, cssClass='codereviewcomment')
+    custom_widget('comment', TextAreaWidget, cssClass='comment-text')
 
     @cachedproperty
     def initial_values(self):

=== modified file 'lib/lp/code/browser/codereviewcomment.py'
--- lib/lp/code/browser/codereviewcomment.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/codereviewcomment.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -33,6 +33,7 @@
 from lp.code.interfaces.codereviewcomment import ICodeReviewComment
 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
 from lp.services.comments.interfaces.conversation import IComment
+from lp.services.comments.browser.messagecomment import MessageComment
 from lp.services.config import config
 from lp.services.librarian.interfaces import ILibraryFileAlias
 from lp.services.propertycache import (
@@ -52,7 +53,7 @@
     """Marker interface for displaying code review comments."""
 
 
-class CodeReviewDisplayComment:
+class CodeReviewDisplayComment(MessageComment):
     """A code review comment or activity or both.
 
     The CodeReviewComment itself does not implement the IComment interface as
@@ -64,7 +65,12 @@
 
     delegates(ICodeReviewComment, 'comment')
 
-    def __init__(self, comment, from_superseded=False):
+    def __init__(self, comment, from_superseded=False, limit_length=True):
+        if limit_length:
+            comment_limit = config.malone.max_comment_size
+        else:
+            comment_limit = None
+        super(CodeReviewDisplayComment, self).__init__(comment_limit)
         self.comment = comment
         get_property_cache(self).has_body = bool(self.comment.message_body)
         self.has_footer = self.comment.vote is not None
@@ -80,26 +86,11 @@
             return ''
 
     @cachedproperty
-    def comment_author(self):
-        """The author of the comment."""
-        return self.comment.message.owner
-
-    @cachedproperty
-    def has_body(self):
-        """Is there body text?"""
-        return bool(self.body_text)
-
-    @cachedproperty
     def body_text(self):
         """Get the body text for the message."""
         return self.comment.message_body
 
     @cachedproperty
-    def comment_date(self):
-        """The date of the comment."""
-        return self.comment.message.datecreated
-
-    @cachedproperty
     def all_attachments(self):
         return self.comment.getAttachments()
 
@@ -114,6 +105,11 @@
         return self.all_attachments[1]
 
 
+def get_message(display_comment):
+    """Adapt an ICodeReviwComment to an IMessage."""
+    return display_comment.comment.message
+
+
 class CodeReviewCommentPrimaryContext:
     """The primary context is the comment is that of the source branch."""
 
@@ -173,7 +169,7 @@
     @cachedproperty
     def comment(self):
         """The decorated code review comment."""
-        return CodeReviewDisplayComment(self.context)
+        return CodeReviewDisplayComment(self.context, limit_length=False)
 
     @property
     def page_description(self):
@@ -239,7 +235,7 @@
 
     schema = IEditCodeReviewComment
 
-    custom_widget('comment', TextAreaWidget, cssClass='codereviewcomment')
+    custom_widget('comment', TextAreaWidget, cssClass='comment-text')
     custom_widget('vote', MyDropWidget)
 
     page_title = 'Reply to code review comment'

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2012-01-09 11:36:12 +0000
+++ lib/lp/code/browser/configure.zcml	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -666,6 +666,11 @@
             name="+comment-footer"
             template="../templates/codereviewcomment-footer.pt"/>
     </browser:pages>
+    <adapter
+      provides="lp.services.messages.interfaces.message.IMessage"
+      for="lp.code.browser.codereviewcomment.ICodeReviewDisplayComment"
+      factory="lp.code.browser.codereviewcomment.get_message"/>
+
     <browser:pages
         for="lp.code.browser.branchmergeproposal.ICodeReviewNewRevisions"
         layer="lp.code.publisher.CodeLayer"

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=F0401
@@ -1114,6 +1114,22 @@
                 "automatically when ready.",
             browser.contents)
 
+    def test_short_conversation_comments_not_truncated(self):
+        """Short comments should not be truncated."""
+        comment = self.factory.makeCodeReviewComment(body='x y' * 100)
+        browser = self.getViewBrowser(comment.branch_merge_proposal)
+        self.assertIn('x y' * 100, browser.contents)
+
+    def test_long_conversation_comments_truncated(self):
+        """Long comments in a conversation should be truncated."""
+        comment = self.factory.makeCodeReviewComment(body='x y' * 2000)
+        url = canonical_url(comment, force_local_path=True)
+        browser = self.getViewBrowser(comment.branch_merge_proposal)
+        self.assertNotIn('x y' * 2000, browser.contents)
+        read_more = Tag(
+            'Read more link', 'a', {'href': url}, text='Read more...')
+        self.assertThat(browser.contents, HTMLContains(read_more))
+
 
 class TestLatestProposalsForEachBranch(TestCaseWithFactory):
     """Confirm that the latest branch is returned."""

=== modified file 'lib/lp/code/browser/tests/test_codereviewcomment.py'
--- lib/lp/code/browser/tests/test_codereviewcomment.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/tests/test_codereviewcomment.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for CodeReviewComments."""
@@ -70,3 +70,10 @@
                 dict(
                     name='description',
                     content=comment.message_body))))
+
+    def test_long_comments_not_truncated(self):
+        """Long comments displayed by themselves are not truncated."""
+        comment = self.factory.makeCodeReviewComment(body='x y' * 2000)
+        browser = self.getViewBrowser(comment)
+        body = Tag('Body text', 'p', text='x y' * 2000)
+        self.assertThat(browser.contents, HTMLContains(body))

=== modified file 'lib/lp/code/templates/codereviewcomment-body.pt'
--- lib/lp/code/templates/codereviewcomment-body.pt	2011-12-22 09:05:46 +0000
+++ lib/lp/code/templates/codereviewcomment-body.pt	2012-01-20 21:22:32 +0000
@@ -3,8 +3,7 @@
    xmlns:metal="http://xml.zope.org/namespaces/metal";
    omit-tag="">
 
-  <tal:message replace="structure view/comment/body_text/fmt:obfuscate-email/fmt:nice_pre" />
-
+  <div tal:content="structure context/@@+comment-body-text" />
   <tal:good-attachments repeat="attachment view/comment/display_attachments">
     <div class="boardComment attachment">
       <div class="boardCommentDetails filename"><a tal:content="attachment/filename" tal:attributes="href attachment/getURL"/></div>

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2012-01-04 12:08:24 +0000
+++ lib/lp/registry/browser/configure.zcml	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -2232,4 +2232,8 @@
         attribute_to_parent="owner" />
 
 </facet>
+<adapter
+  provides="lp.services.messages.interfaces.message.IMessage"
+  for="lp.registry.browser.distroseriesdifference.IDistroSeriesDifferenceDisplayComment"
+  factory="lp.registry.browser.distroseriesdifference.get_message"/>
 </configure>

=== modified file 'lib/lp/registry/browser/distroseriesdifference.py'
--- lib/lp/registry/browser/distroseriesdifference.py	2012-01-05 20:11:40 +0000
+++ lib/lp/registry/browser/distroseriesdifference.py	2012-01-20 21:22:32 +0000
@@ -45,6 +45,7 @@
     IComment,
     IConversation,
     )
+from lp.services.comments.browser.messagecomment import MessageComment
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
     LaunchpadView,
@@ -241,20 +242,23 @@
             self.show_package_diffs_request_link)
 
 
-class DistroSeriesDifferenceDisplayComment:
+class IDistroSeriesDifferenceDisplayComment(IComment):
+    """Marker interface."""
+
+
+class DistroSeriesDifferenceDisplayComment(MessageComment):
     """Used simply to provide `IComment` for rendering."""
-    implements(IComment)
-
-    has_body = True
-    has_footer = False
-    display_attachments = False
-    extra_css_class = ''
+    implements(IDistroSeriesDifferenceDisplayComment)
 
     def __init__(self, comment):
         """Setup the attributes required by `IComment`."""
-        self.comment_author = comment.comment_author
-        self.comment_date = comment.comment_date
-        self.body_text = comment.body_text
+        super(DistroSeriesDifferenceDisplayComment, self).__init__(None)
+        self.comment = comment
+
+
+def get_message(comment):
+    """Adapter from IDistroSeriesDifferenceDisplayComment to IMessage."""
+    return comment.comment.message
 
 
 class CommentXHTMLRepresentation(LaunchpadView):

=== modified file 'lib/lp/scripts/harness.py'
--- lib/lp/scripts/harness.py	2012-01-20 21:22:32 +0000
+++ lib/lp/scripts/harness.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2004-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Scripts for starting a Python prompt with Launchpad initialized.
@@ -20,6 +20,7 @@
 import readline
 import rlcompleter
 import sys
+import webbrowser
 
 from pytz import utc
 from storm.expr import *
@@ -87,6 +88,17 @@
     # Having a factory instance is handy.
     factory = LaunchpadObjectFactory()
 
+    def browser_open(obj, *args, **kwargs):
+        """Open a (possibly newly-created) object's view in a web browser.
+
+        Accepts the same parameters as canonical_url.
+
+        Performs a commit before invoking the browser, so
+        "browser_open(factory.makeFoo())" works.
+        """
+        transaction.commit()
+        webbrowser.open(canonical_url(obj, *args, **kwargs))
+
     # Silence unused name warnings
     factory, store
 

=== modified file 'lib/lp/services/comments/browser/configure.zcml'
--- lib/lp/services/comments/browser/configure.zcml	2010-09-17 11:42:29 +0000
+++ lib/lp/services/comments/browser/configure.zcml	2012-01-20 21:22:32 +0000
@@ -1,5 +1,6 @@
-<!-- Copyright 2009 Canonical Ltd.  This software is licensed under the
-     GNU Affero General Public License version 3 (see the file LICENSE).
+<!-- Copyright 2009, 2010, 2012 Canonical Ltd.  This software is licensed
+     under the GNU Affero General Public License version 3 (see the file
+     LICENSE).
 -->
 
 <configure
@@ -24,9 +25,14 @@
       <browser:page
           name="+comment-header"
           template="../templates/comment-header.pt"/>
+      <!-- Default comment body is just the body text, but implementations may
+      vary -->
       <browser:page
           name="+comment-body"
           template="../templates/comment-body.pt"/>
+      <browser:page
+          name="+comment-body-text"
+          template="../templates/comment-body.pt"/>
   </browser:pages>
 
 </configure>

=== added file 'lib/lp/services/comments/browser/messagecomment.py'
--- lib/lp/services/comments/browser/messagecomment.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/comments/browser/messagecomment.py	2012-01-20 21:22:32 +0000
@@ -0,0 +1,61 @@
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = ['MessageComment']
+
+
+from lp.services.messages.interfaces.message import IMessage
+from lp.services.propertycache import cachedproperty
+
+
+class MessageComment:
+    """Mixin to partially implement IComment in terms of IMessage."""
+
+    extra_css_class = ''
+
+    has_footer = False
+
+    def __init__(self, comment_limit):
+        self.comment_limit = comment_limit
+
+    @property
+    def display_attachments(self):
+        return []
+
+    @cachedproperty
+    def comment_author(self):
+        """The author of the comment."""
+        return IMessage(self).owner
+
+    @cachedproperty
+    def has_body(self):
+        """Is there body text?"""
+        return bool(self.body_text)
+
+    @cachedproperty
+    def comment_date(self):
+        """The date of the comment."""
+        return IMessage(self).datecreated
+
+    @property
+    def body_text(self):
+        return IMessage(self).text_contents
+
+    @property
+    def too_long(self):
+        if self.comment_limit is None:
+            return False
+        return len(self.body_text) > self.comment_limit
+
+    @cachedproperty
+    def text_for_display(self):
+        if not self.too_long:
+            return self.body_text
+        # Note here that we truncate at comment_limit, and not
+        # comment_limit - 3; while it would be nice to account for
+        # the ellipsis, this breaks down when the comment limit is
+        # less than 3 (which can happen in a testcase) and it makes
+        # counting the strings harder.
+        return "%s..." % self.body_text[:self.comment_limit]

=== modified file 'lib/lp/services/comments/doc/conversation.txt'
--- lib/lp/services/comments/doc/conversation.txt	2012-01-20 21:22:32 +0000
+++ lib/lp/services/comments/doc/conversation.txt	2012-01-20 21:22:32 +0000
@@ -36,7 +36,10 @@
  * +comment-header - the top part of the comment
      normally something like "Bob wrote 4 seconds ago"
  * +comment-body - the main content
-     Only rendered if IComment.has_body is true.
+     Only rendered if IComment.has_body is true.  By default, is the same as
+     +comment-body-text
+ * +comment-body-text - the main content
+     The textual portion of the message body, suitably escaped and linkified.
  * +comment-footer - associated activity or other footer info, like
      bug activity or code reviews.
      Only rendered if IComment.has_footer is true

=== modified file 'lib/lp/services/comments/interfaces/conversation.py'
--- lib/lp/services/comments/interfaces/conversation.py	2011-12-24 16:54:44 +0000
+++ lib/lp/services/comments/interfaces/conversation.py	2012-01-20 21:22:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces to do with conversations on Launchpad entities."""
@@ -39,6 +39,13 @@
         description=_("Does the comment have a footer?"),
         readonly=True)
 
+    too_long = Bool(
+        title=u'Whether the comment body is too long to display in full.',
+        readonly=True)
+
+    text_for_display = Text(
+        title=u'The comment text to be displayed in the UI.', readonly=True)
+
     body_text = Text(
         description=_("The body text of the comment."),
         readonly=True)

=== modified file 'lib/lp/services/comments/templates/comment-body.pt'
--- lib/lp/services/comments/templates/comment-body.pt	2010-09-17 10:50:51 +0000
+++ lib/lp/services/comments/templates/comment-body.pt	2012-01-20 21:22:32 +0000
@@ -3,6 +3,10 @@
    xmlns:metal="http://xml.zope.org/namespaces/metal";
    omit-tag="">
 
-  <tal:message replace="structure context/body_text/fmt:obfuscate-email/fmt:nice_pre" />
+  <div class="comment-text" itemprop="commentText" tal:content="structure
+  context/text_for_display/fmt:obfuscate-email/fmt:email-to-html" />
+  <p tal:condition="context/too_long">
+    <a tal:attributes="href context/fmt:url">Read more...</a>
+  </p>
 
 </tal:root>

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2012-01-18 12:04:16 +0000
+++ lib/lp/testing/factory.py	2012-01-20 21:22:32 +0000
@@ -2,7 +2,7 @@
 # NOTE: The first line above must stay first; do not move the copyright
 # notice to the top.  See http://www.python.org/dev/peps/pep-0263/.
 #
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=F0401
@@ -2323,9 +2323,10 @@
             else:
                 merge_proposal = self.makeBranchMergeProposal(
                     registrant=sender)
-        return merge_proposal.createComment(
-            sender, subject, body, vote, vote_tag, parent,
-            _date_created=date_created)
+        with person_logged_in(sender):
+            return merge_proposal.createComment(
+                sender, subject, body, vote, vote_tag, parent,
+                _date_created=date_created)
 
     def makeCodeReviewVoteReference(self):
         bmp = removeSecurityProxy(self.makeBranchMergeProposal())