Colin Watson has proposed merging lp:~cjwatson/launchpad/bmp-hide-comments into lp:launchpad with lp:~cjwatson/launchpad/bmp-remove-old-comment-threading as a prerequisite.
Commit message: Add spam controls to code review comments. Requested reviews: Launchpad code reviewers (launchpad-reviewers) Related bugs: Bug #1680746 in Launchpad itself: "No way to remove merge proposal comment spam" https://bugs.launchpad.net/launchpad/+bug/1680746 For more details, see: https://code.launchpad.net/~cjwatson/launchpad/bmp-hide-comments/+merge/344225 -- Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/bmp-hide-comments into lp:launchpad.
=== modified file 'lib/lp/app/javascript/comment.js' --- lib/lp/app/javascript/comment.js 2017-09-22 19:41:16 +0000 +++ lib/lp/app/javascript/comment.js 2018-04-25 12:03:26 +0000 @@ -72,6 +72,16 @@ }); }, + /** + * Bind the UI to a new comment. + * + * @method bind_new_comment + */ + bind_new_comment: function(comment_node) { + comment_node.delegate( + 'click', this.toggle_hidden, 'a.mark-spam', this); + }, + destructor: function() { var comment_list_container = this.get('comment_list_container'); comment_list_container.detachAll(); @@ -475,7 +485,7 @@ var comment = Y.Node.create(message_html); conversation.appendChild(comment); this.reset_contents(); - this.fire('CodeReviewComment.appended'); + this.fire('CodeReviewComment.appended', comment); Y.lp.anim.green_flash({node: comment}).run(); }, === modified file 'lib/lp/app/javascript/tests/test_comment.html' --- lib/lp/app/javascript/tests/test_comment.html 2012-10-26 09:54:28 +0000 +++ lib/lp/app/javascript/tests/test_comment.html 2018-04-25 12:03:26 +0000 @@ -1,6 +1,6 @@ <!DOCTYPE html> <!-- -Copyright 2012 Canonical Ltd. This software is licensed under the +Copyright 2012-2018 Canonical Ltd. This software is licensed under the GNU Affero General Public License version 3 (see the file LICENSE). --> @@ -32,7 +32,9 @@ <script type="text/javascript" src="../../../../../build/js/lp/app/client.js"></script> <script type="text/javascript" - src="../../../../../build/js/lp/app/client.js"></script> + src="../../../../../build/js/lp/app/anim/anim.js"></script> + <script type="text/javascript" + src="../../../../../build/js/lp/app/extras/extras.js"></script> <!-- The module under test. --> <script type="text/javascript" src="../comment.js"></script> === modified file 'lib/lp/app/javascript/tests/test_comment.js' --- lib/lp/app/javascript/tests/test_comment.js 2017-09-22 19:41:16 +0000 +++ lib/lp/app/javascript/tests/test_comment.js 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -/* Copyright 2012 Canonical Ltd. This software is licensed under the +/* Copyright 2012-2018 Canonical Ltd. This software is licensed under the * GNU Affero General Public License version 3 (see the file LICENSE). */ YUI.add('lp.app.comment.test', function (Y) { @@ -120,6 +120,48 @@ comment.insert_comment_HTML('<span id="fake"></span>'); Y.Assert.isTrue(reset_contents_called); Y.Assert.isObject(Y.one('#fake')); + }, + + test_insert_code_review_comment_html: function () { + // Test CodeReviewComment.insert_comment_HTML + window.LP = { + cache: { + context: { self_link: '/mp/1/' } + } + }; + Y.one('body').appendChild( + Y.Node.create('<div id="conversation"/>')); + var mock_client = new Y.lp.testing.helpers.LPClient(); + mock_client.named_post.args = []; + mock_client.get.args = []; + var comment = new Y.lp.app.comment.CodeReviewComment({ + animate: false, + lp_client: mock_client + }); + var reset_contents_called = false; + comment.reset_contents = function () { + reset_contents_called = true; + }; + var new_comment_node = null; + comment.on('CodeReviewComment.appended', function (comment_node) { + new_comment_node = comment_node; + }); + var green_flash = Y.lp.anim.green_flash; + var green_flashed = false; + Y.lp.anim.green_flash = function () { + return { + run: function () { + green_flashed = true; + } + }; + }; + comment.insert_comment_HTML('<span id="fake"></span>'); + Y.Assert.isTrue(reset_contents_called); + Y.Assert.isObject(new_comment_node); + Y.Assert.isObject(Y.one('#fake')); + Y.Assert.areEqual(new_comment_node.get('id'), 'fake'); + Y.Assert.isTrue(green_flashed); + Y.lp.anim.green_flash = green_flash; } })); === modified file 'lib/lp/code/browser/branchmergeproposal.py' --- lib/lp/code/browser/branchmergeproposal.py 2018-04-25 12:03:26 +0000 +++ lib/lp/code/browser/branchmergeproposal.py 2018-04-25 12:03:26 +0000 @@ -123,6 +123,7 @@ from lp.services.webapp.authorization import check_permission from lp.services.webapp.breadcrumb import Breadcrumb from lp.services.webapp.escaping import structured +from lp.services.webapp.interfaces import ILaunchBag from lp.services.webapp.menu import NavigationMenu @@ -436,9 +437,14 @@ except ValueError: return None try: - return self.context.getComment(id) + comment = self.context.getComment(id) except WrongBranchMergeProposal: return None + user = getUtility(ILaunchBag).user + if comment.visible or comment.userCanSetCommentVisibility(user): + return comment + else: + return None @stepthrough("+preview-diff") def traverse_preview_diff(self, id): @@ -591,6 +597,8 @@ self.comment_date = None self.display_attachments = False self.index = None + self.visible = True + self.show_spam_controls = False def download(self, request): pass @@ -661,6 +669,8 @@ source = merge_proposal.merge_source if IBranch.providedBy(source): source = DecoratedBranch(source) + user = getUtility(ILaunchBag).user + strip_invisible = not merge_proposal.userCanSetCommentVisibility(user) comments = [] if (getFeatureFlag('code.incremental_diffs.enabled') and merge_proposal.source_branch is not None): @@ -688,6 +698,8 @@ for comment in merge_proposal.all_comments) merge_proposal = merge_proposal.supersedes comments = sorted(comments, key=operator.attrgetter('date')) + if strip_invisible: + comments = [c for c in comments if c.visible or c.author == user] self._populate_previewdiffs(comments) return CodeReviewConversation(comments) === modified file 'lib/lp/code/browser/codereviewcomment.py' --- lib/lp/code/browser/codereviewcomment.py 2015-07-09 20:06:17 +0000 +++ lib/lp/code/browser/codereviewcomment.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). __metaclass__ = type @@ -50,6 +50,7 @@ LaunchpadView, Link, ) +from lp.services.webapp.interfaces import ILaunchBag class ICodeReviewDisplayComment(IComment, ICodeReviewComment): @@ -85,10 +86,11 @@ @property def extra_css_class(self): + css_classes = ( + super(CodeReviewDisplayComment, self).extra_css_class.split()) if self.from_superseded: - return 'from-superseded' - else: - return '' + css_classes.append('from-superseded') + return ' '.join(css_classes) @cachedproperty def previewdiff_id(self): @@ -121,6 +123,11 @@ def download_url(self): return canonical_url(self.comment, view_name='+download') + @cachedproperty + def show_spam_controls(self): + user = getUtility(ILaunchBag).user + return self.comment.userCanSetCommentVisibility(user) + def get_message(display_comment): """Adapt an ICodeReviwComment to an IMessage.""" === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-03-16 21:20:00 +0000 +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-04-25 12:03:26 +0000 @@ -26,6 +26,10 @@ Tag, Within, ) +from testscenarios import ( + load_tests_apply_scenarios, + WithScenarios, + ) from testtools.matchers import ( ContainsDict, DocTestMatches, @@ -77,6 +81,10 @@ make_merge_proposal_without_reviewers, ) from lp.code.xmlrpc.git import GitAPI +from lp.coop.answersbugs.visibility import ( + TestHideMessageControlMixin, + TestMessageVisibilityMixin, + ) from lp.registry.enums import ( PersonVisibility, TeamMembershipPolicy, @@ -1899,6 +1907,64 @@ git_proposal.merge_source.commit_sha1, view.source_revid) +class TestCodeReviewCommentVisibility( + WithScenarios, BrowserTestCase, TestMessageVisibilityMixin): + + layer = DatabaseFunctionalLayer + + scenarios = [ + ('bzr', {'git': False}), + ('git', {'git': True}), + ] + + def makeHiddenMessage(self, comment_owner=None): + """See `TestMessageVisibilityMixin`.""" + if self.git: + self.useFixture(GitHostingFixture()) + comment = self.factory.makeCodeReviewComment( + sender=comment_owner, body=self.comment_text, git=self.git) + with admin_logged_in(): + comment.message.visible = False + return comment.branch_merge_proposal + + def getView(self, context, user=None, no_login=False): + """See `TestMessageVisibilityMixin`.""" + return self.getViewBrowser( + context=context, user=user, no_login=no_login) + + +class TestCodeReviewCommentHideControls( + WithScenarios, BrowserTestCase, TestHideMessageControlMixin): + + layer = DatabaseFunctionalLayer + + scenarios = [ + ('bzr', {'git': False}), + ('git', {'git': True}), + ] + + def getContext(self, comment_owner=None): + """See `TestHideMessageControlMixin`.""" + if self.git: + self.useFixture(GitHostingFixture()) + comment = self.factory.makeCodeReviewComment( + sender=comment_owner, git=self.git) + self.control_text = 'mark-spam-%d' % comment.id + return comment.branch_merge_proposal + + def getView(self, context, user=None, no_login=False): + """See `TestHideMessageControlMixin`.""" + return self.getViewBrowser( + context=context, user=user, no_login=no_login) + + def test_comment_owner_sees_hide_control(self): + user = self.factory.makePerson() + context = self.getContext(comment_owner=user) + view = self.getView(context=context, user=user) + hide_link = find_tag_by_id(view.contents, self.control_text) + self.assertIsNot(None, hide_link) + + class TestCommentAttachmentRendering(TestCaseWithFactory): """Test diff attachments are rendered correctly.""" @@ -2308,3 +2374,6 @@ def _makeBranchMergeProposal(self, **kwargs): return self.factory.makeBranchMergeProposalForGit(**kwargs) + + +load_tests = load_tests_apply_scenarios === modified file 'lib/lp/code/browser/tests/test_codereviewcomment.py' --- lib/lp/code/browser/tests/test_codereviewcomment.py 2017-10-04 01:16:22 +0000 +++ lib/lp/code/browser/tests/test_codereviewcomment.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2017 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Unit tests for CodeReviewComments.""" @@ -24,9 +24,12 @@ from lp.code.interfaces.codereviewinlinecomment import ( ICodeReviewInlineCommentSet, ) +from lp.services.propertycache import clear_property_cache from lp.services.webapp import canonical_url from lp.testing import ( + admin_logged_in, BrowserTestCase, + celebrity_logged_in, person_logged_in, StormStatementRecorder, TestCaseWithFactory, @@ -52,6 +55,34 @@ verifyObject(ICodeReviewDisplayComment, display_comment) + def test_extra_css_classes_visibility(self): + author = self.factory.makePerson() + comment = self.factory.makeCodeReviewComment(sender=author) + display_comment = CodeReviewDisplayComment(comment) + self.assertEqual('', display_comment.extra_css_class) + with person_logged_in(author): + display_comment.message.setVisible(False) + self.assertEqual( + 'adminHiddenComment', display_comment.extra_css_class) + + def test_show_spam_controls_permissions(self): + # Admins, registry experts, and the author of the comment itself can + # hide comments, but other people can't. + author = self.factory.makePerson() + comment = self.factory.makeCodeReviewComment(sender=author) + display_comment = CodeReviewDisplayComment(comment) + with person_logged_in(author): + self.assertTrue(display_comment.show_spam_controls) + clear_property_cache(display_comment) + with person_logged_in(self.factory.makePerson()): + self.assertFalse(display_comment.show_spam_controls) + clear_property_cache(display_comment) + with celebrity_logged_in('registry_experts'): + self.assertTrue(display_comment.show_spam_controls) + clear_property_cache(display_comment) + with admin_logged_in(): + self.assertTrue(display_comment.show_spam_controls) + class TestCodeReviewCommentInlineComments(TestCaseWithFactory): """Test `CodeReviewDisplayComment` integration with inline-comments.""" === modified file 'lib/lp/code/interfaces/branchmergeproposal.py' --- lib/lp/code/interfaces/branchmergeproposal.py 2018-03-16 21:20:00 +0000 +++ lib/lp/code/interfaces/branchmergeproposal.py 2018-04-25 12:03:26 +0000 @@ -376,7 +376,41 @@ @operation_returns_entry(Interface) @export_read_operation() def getComment(id): - """Return the CodeReviewComment with the specified ID.""" + """Return the CodeReviewComment with the specified ID. + + :raises WrongBranchMergeProposal: if the comment with this ID is not + on this merge proposal. + """ + + def userCanSetCommentVisibility(user): + """Can `user` set code review comment visibility? + + Admins and registry experts can set the visibility of any code + review comment. + + Comment authors can also set the visibility of their own comments, + but that is not checked here; this method determines whether + arbitrary users can set the visibility of comments they did not make + themselves. + """ + + @operation_parameters( + # Really comment_id, but this lets us use common JavaScript code. + comment_number=Int(title=_('The comment ID.'), required=True), + visible=Bool(title=_('Show this comment?'), required=True)) + @call_with(user=REQUEST_USER) + @export_write_operation() + @operation_for_version('devel') + def setCommentVisibility(user, comment_number, visible): + """Set the visible attribute on a code review comment. + + This is restricted to Launchpad admins, registry experts, and + comment authors, and will return a HTTP Error 401: Unauthorized + error for non-admin callers. + + :raises WrongBranchMergeProposal: if the comment with this ID is not + on this merge proposal. + """ @call_with(user=REQUEST_USER) # Really IBugTask. === modified file 'lib/lp/code/interfaces/codereviewcomment.py' --- lib/lp/code/interfaces/codereviewcomment.py 2013-01-07 02:40:55 +0000 +++ lib/lp/code/interfaces/codereviewcomment.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """CodeReviewComment interfaces.""" @@ -16,6 +16,7 @@ from lazr.restful.fields import Reference from zope.interface import Interface from zope.schema import ( + Bool, Choice, Datetime, Int, @@ -87,6 +88,16 @@ title=_('The message as quoted in email.'), readonly=True)) + visible = Bool(title=_('Whether this comment is visible.')) + + def userCanSetCommentVisibility(user): + """Can `user` set the visibility of this comment? + + Admins and registry experts can set the visibility of any code + review comment. Comment authors can set the visibility of their own + comments. + """ + class ICodeReviewCommentDeletion(Interface): """This interface provides deletion of CodeReviewComments. === modified file 'lib/lp/code/model/branchmergeproposal.py' --- lib/lp/code/model/branchmergeproposal.py 2017-11-24 17:22:34 +0000 +++ lib/lp/code/model/branchmergeproposal.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2017 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Database class for branch merge proposals.""" @@ -43,6 +43,7 @@ from zope.error.interfaces import IErrorReportingUtility from zope.event import notify from zope.interface import implementer +from zope.security.interfaces import Unauthorized from lp.app.enums import PRIVATE_INFORMATION_TYPES from lp.app.interfaces.launchpad import ILaunchpadCelebrities @@ -97,6 +98,7 @@ validate_public_person, ) from lp.registry.interfaces.product import IProduct +from lp.registry.interfaces.role import IPersonRoles from lp.registry.model.person import Person from lp.services.config import config from lp.services.database.bulk import ( @@ -560,14 +562,30 @@ return CodeReviewComment.selectBy(branch_merge_proposal=self.id) def getComment(self, id): - """See `IBranchMergeProposal`. - - This function can raise WrongBranchMergeProposal.""" + """See `IBranchMergeProposal`.""" comment = CodeReviewComment.get(id) if comment.branch_merge_proposal != self: raise WrongBranchMergeProposal return comment + def userCanSetCommentVisibility(self, user): + """See `IBranchMergeProposal`.""" + if user is None: + return False + roles = IPersonRoles(user) + return roles.in_admin or roles.in_registry_experts + + def setCommentVisibility(self, user, comment_number, visible): + """See `IBranchMergeProposal`.""" + comment = CodeReviewComment.get(comment_number) + if comment.branch_merge_proposal != self: + raise WrongBranchMergeProposal + if not comment.userCanSetCommentVisibility(user): + raise Unauthorized( + "User %s cannot hide or show code review comments." % + (user.name if user is not None else "<anonymous>")) + comment.message.setVisible(visible) + def getVoteReference(self, id): """See `IBranchMergeProposal`. === modified file 'lib/lp/code/model/codereviewcomment.py' --- lib/lp/code/model/codereviewcomment.py 2015-10-19 10:56:16 +0000 +++ lib/lp/code/model/codereviewcomment.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """The database implementation class for CodeReviewComment.""" @@ -124,3 +124,13 @@ if self.message.raw is None: return None return signed_message_from_string(self.message.raw.read()) + + @property + def visible(self): + return self.message.visible + + def userCanSetCommentVisibility(self, user): + """See `ICodeReviewComment`.""" + return ( + self.branch_merge_proposal.userCanSetCommentVisibility(user) or + (user is not None and user.inTeam(self.author))) === modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py' --- lib/lp/code/model/tests/test_branchmergeproposal.py 2018-01-02 16:10:26 +0000 +++ lib/lp/code/model/tests/test_branchmergeproposal.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2017 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Tests for BranchMergeProposals.""" @@ -32,6 +32,7 @@ ) import transaction from zope.component import getUtility +from zope.security.interfaces import Unauthorized from zope.security.proxy import removeSecurityProxy from lp.app.enums import InformationType @@ -607,6 +608,57 @@ self.merge_proposal2.getComment, self.comment.id) +class TestMergeProposalSetCommentVisibility(TestCaseWithFactory): + + layer = DatabaseFunctionalLayer + + def test_anonymous(self): + comment = self.factory.makeCodeReviewComment() + self.assertRaisesWithContent( + Unauthorized, + "User <anonymous> cannot hide or show code review comments.", + comment.branch_merge_proposal.setCommentVisibility, + user=None, comment_number=comment.id, visible=False) + + def test_random_user(self): + comment = self.factory.makeCodeReviewComment() + person = self.factory.makePerson() + self.assertRaisesWithContent( + Unauthorized, + "User %s cannot hide or show code review comments." % person.name, + comment.branch_merge_proposal.setCommentVisibility, + user=person, comment_number=comment.id, visible=False) + + def test_comment_author(self): + comment = self.factory.makeCodeReviewComment() + another_comment = self.factory.makeCodeReviewComment( + merge_proposal=comment.branch_merge_proposal) + comment.branch_merge_proposal.setCommentVisibility( + user=comment.author, comment_number=comment.id, visible=False) + self.assertFalse(comment.visible) + self.assertTrue(another_comment.visible) + + def test_registry_expert(self): + comment = self.factory.makeCodeReviewComment() + another_comment = self.factory.makeCodeReviewComment( + merge_proposal=comment.branch_merge_proposal) + comment.branch_merge_proposal.setCommentVisibility( + user=self.factory.makeRegistryExpert(), comment_number=comment.id, + visible=False) + self.assertFalse(comment.visible) + self.assertTrue(another_comment.visible) + + def test_admin(self): + comment = self.factory.makeCodeReviewComment() + another_comment = self.factory.makeCodeReviewComment( + merge_proposal=comment.branch_merge_proposal) + comment.branch_merge_proposal.setCommentVisibility( + user=self.factory.makeAdministrator(), comment_number=comment.id, + visible=False) + self.assertFalse(comment.visible) + self.assertTrue(another_comment.visible) + + class TestMergeProposalGetVoteReference(TestCaseWithFactory): """Tester for `BranchMergeProposal.getComment`.""" === modified file 'lib/lp/code/templates/branchmergeproposal-index.pt' --- lib/lp/code/templates/branchmergeproposal-index.pt 2016-05-19 01:04:56 +0000 +++ lib/lp/code/templates/branchmergeproposal-index.pt 2018-04-25 12:03:26 +0000 @@ -218,18 +218,25 @@ Y.on('load', function() { var logged_in = LP.links['me'] !== undefined; - var ic = Y.lp.code.branchmergeproposal.inlinecomments; - var diffnav = new ic.DiffNav({srcNode: Y.one('#review-diff')}); + var comment = null; if (logged_in) { - var comment = new Y.lp.app.comment.CodeReviewComment(); + comment = new Y.lp.app.comment.CodeReviewComment(); comment.render(); - comment.on('CodeReviewComment.appended', function () { - diffnav.update_on_new_comment(); - }); - Y.lp.code.branchmergeproposal.status.connect_status(conf); - } - Y.lp.code.branchmergeproposal.reviewcomment.connect_links(); - diffnav.render(); + } + + var review_diff_node = Y.one('#review-diff'); + if (review_diff_node !== null) { + var ic = Y.lp.code.branchmergeproposal.inlinecomments; + var diffnav = new ic.DiffNav({srcNode: review_diff_node}); + if (comment !== null) { + comment.on('CodeReviewComment.appended', function () { + diffnav.update_on_new_comment(); + }); + Y.lp.code.branchmergeproposal.status.connect_status(conf); + } + Y.lp.code.branchmergeproposal.reviewcomment.connect_links(); + diffnav.render(); + } if (Y.Lang.isValue(LP.cache.merge_proposal_event_key)) { var upt = Y.lp.code.branchmergeproposal.updater; @@ -247,6 +254,15 @@ diffnav.render(); }); } + + LP.cache.comment_context = LP.cache.context; + var cl = new Y.lp.app.comment.CommentList({ + comment_list_container: Y.one('#conversation') + }); + if (comment !== null) { + comment.on('CodeReviewComment.appended', cl.bind_new_comment); + } + cl.render(); }, window); Y.on('domready', function() { === modified file 'lib/lp/registry/browser/distroseriesdifference.py' --- lib/lp/registry/browser/distroseriesdifference.py 2015-07-09 12:18:51 +0000 +++ lib/lp/registry/browser/distroseriesdifference.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2010-2012 Canonical Ltd. This software is licensed under the +# Copyright 2010-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Browser views for DistroSeriesDifferences.""" @@ -255,6 +255,8 @@ download_url = None + visible = True + def __init__(self, comment): """Setup the attributes required by `IComment`.""" super(DistroSeriesDifferenceDisplayComment, self).__init__(None) === modified file 'lib/lp/services/comments/browser/messagecomment.py' --- lib/lp/services/comments/browser/messagecomment.py 2012-01-27 18:56:36 +0000 +++ lib/lp/services/comments/browser/messagecomment.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). __metaclass__ = type @@ -22,6 +22,15 @@ self.comment_limit = comment_limit @property + def extra_css_class(self): + if not self.visible: + # If a comment that isn't visible is being rendered, it's being + # rendered for an admin or registry_expert. + return 'adminHiddenComment' + else: + return '' + + @property def display_attachments(self): return [] @@ -64,3 +73,5 @@ # less than 3 (which can happen in a testcase) and it makes # counting the strings harder. return "%s..." % self.body_text[:self.comment_limit] + + show_spam_controls = False === modified file 'lib/lp/services/comments/interfaces/conversation.py' --- lib/lp/services/comments/interfaces/conversation.py 2012-02-01 15:26:32 +0000 +++ lib/lp/services/comments/interfaces/conversation.py 2018-04-25 12:03:26 +0000 @@ -1,4 +1,4 @@ -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the +# Copyright 2009-2018 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.""" @@ -73,6 +73,11 @@ description=_("Should attachments be displayed for this comment."), readonly=True) + visible = Bool(title=_("Whether this comment is visible.")) + + show_spam_controls = Bool( + title=_("Whether to show spam controls for this comment.")) + class IConversation(Interface): """A conversation has a number of comments.""" === modified file 'lib/lp/services/comments/templates/comment.pt' --- lib/lp/services/comments/templates/comment.pt 2014-05-29 07:27:25 +0000 +++ lib/lp/services/comments/templates/comment.pt 2018-04-25 12:03:26 +0000 @@ -25,6 +25,12 @@ <div class="boardCommentFooter" tal:define="link context/menu:context/reply | nothing" tal:condition="link/enabled | nothing"> + <a tal:condition="context/show_spam_controls" + tal:attributes="id string:mark-spam-${context/index};" + class="js-action mark-spam" href="#"> + <tal:not-spam condition="not: context/visible">Unhide</tal:not-spam> + <tal:spam condition="context/visible">Hide</tal:spam> + </a> <a itemprop="replyToUrl" tal:attributes="href link/fmt:url" tal:content="link/escapedtext">
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp