=== modified file 'lib/lp/answers/browser/tests/test_questionmessages.py'
--- lib/lp/answers/browser/tests/test_questionmessages.py 2011-05-27 21:12:25 +0000
+++ lib/lp/answers/browser/tests/test_questionmessages.py 2011-11-17 23:00:32 +0000
@@ -50,7 +50,7 @@
control_text = 'mark-spam-0'
- def getContext(self):
+ def getContext(self, comment_owner=None):
"""Required by the mixin."""
administrator = getUtility(ILaunchpadCelebrities).admin.teamowner
question = self.factory.makeQuestion()
=== modified file 'lib/lp/bugs/browser/bugcomment.py'
--- lib/lp/bugs/browser/bugcomment.py 2011-09-15 16:31:49 +0000
+++ lib/lp/bugs/browser/bugcomment.py 2011-11-17 23:00:32 +0000
@@ -46,13 +46,15 @@
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
from canonical.launchpad.webapp.interfaces import ILaunchBag
from lp.bugs.interfaces.bugmessage import IBugComment
+from lp.services.features import getFeatureFlag
COMMENT_ACTIVITY_GROUPING_WINDOW = timedelta(minutes=5)
def build_comments_from_chunks(
- bugtask, truncate=False, slice_info=None, show_spam_controls=False):
+ bugtask, truncate=False, slice_info=None, show_spam_controls=False,
+ user=None):
"""Build BugComments from MessageChunks.
:param truncate: Perform truncation of large messages.
@@ -66,7 +68,7 @@
if bug_comment is None:
bug_comment = BugComment(
bugmessage.index, message, bugtask, visible=message.visible,
- show_spam_controls=show_spam_controls)
+ show_spam_controls=show_spam_controls, user=user)
comments[message.id] = bug_comment
# This code path is currently only used from a BugTask view which
# has already loaded all the bug watches. If we start lazy loading
@@ -176,7 +178,7 @@
def __init__(
self, index, message, bugtask, activity=None,
- visible=True, show_spam_controls=False):
+ visible=True, show_spam_controls=False, user=None):
self.index = index
self.bugtask = bugtask
@@ -199,7 +201,12 @@
self.synchronized = False
self.visible = visible
- self.show_spam_controls = show_spam_controls
+ # We use a feature flag to control users deleting their own comments.
+ user_owns_comment = False
+ flag = 'disclosure.users_hide_own_bug_comments.enabled'
+ 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
@property
def show_for_admin(self):
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-11-17 17:17:38 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-11-17 23:00:32 +0000
@@ -332,7 +332,7 @@
def get_comments_for_bugtask(bugtask, truncate=False, for_display=False,
- slice_info=None, show_spam_controls=False):
+ slice_info=None, show_spam_controls=False, user=None):
"""Return BugComments related to a bugtask.
This code builds a sorted list of BugComments in one shot,
@@ -345,7 +345,8 @@
to retrieve.
"""
comments = build_comments_from_chunks(bugtask, truncate=truncate,
- slice_info=slice_info, show_spam_controls=show_spam_controls)
+ slice_info=slice_info, show_spam_controls=show_spam_controls,
+ user=user)
# TODO: further fat can be shaved off here by limiting the attachments we
# query to those that slice_info would include.
for attachment in bugtask.bug.attachments_unpopulated:
@@ -752,11 +753,12 @@
return self._getComments()
def _getComments(self, slice_info=None):
- show_spam_controls = check_permission(
- 'launchpad.Admin', self.context.bug)
+ bug = self.context.bug
+ show_spam_controls = bug.userCanSetCommentVisibility(self.user)
return get_comments_for_bugtask(
self.context, truncate=True, slice_info=slice_info,
- for_display=True, show_spam_controls=show_spam_controls)
+ for_display=True, show_spam_controls=show_spam_controls,
+ user=self.user)
@cachedproperty
def interesting_activity(self):
=== modified file 'lib/lp/bugs/browser/tests/test_bugcomment.py'
--- lib/lp/bugs/browser/tests/test_bugcomment.py 2011-05-27 21:12:25 +0000
+++ lib/lp/bugs/browser/tests/test_bugcomment.py 2011-11-17 23:00:32 +0000
@@ -13,7 +13,9 @@
from pytz import utc
from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+from canonical.launchpad.testing.pages import find_tag_by_id
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.bugs.browser.bugcomment import group_comments_with_activity
@@ -21,6 +23,7 @@
TestHideMessageControlMixin,
TestMessageVisibilityMixin,
)
+from lp.services.features.testing import FeatureFixture
from lp.testing import (
BrowserTestCase,
celebrity_logged_in,
@@ -220,12 +223,14 @@
layer = DatabaseFunctionalLayer
- def getContext(self):
+ feature_flag = {'disclosure.users_hide_own_bug_comments.enabled': 'on'}
+
+ def getContext(self, comment_owner=None):
"""Required by the mixin."""
administrator = getUtility(ILaunchpadCelebrities).admin.teamowner
bug = self.factory.makeBug()
with person_logged_in(administrator):
- self.factory.makeBugComment(bug=bug)
+ self.factory.makeBugComment(bug=bug, owner=comment_owner)
return bug
def getView(self, context, user=None, no_login=False):
@@ -235,3 +240,50 @@
user=user,
no_login=no_login)
return view
+
+ def _test_hide_link_visible(self, context, user):
+ view = self.getView(context=context, user=user)
+ hide_link = find_tag_by_id(view.contents, self.control_text)
+ self.assertIs(None, hide_link)
+ with FeatureFixture(self.feature_flag):
+ view = self.getView(context=context, user=user)
+ hide_link = find_tag_by_id(view.contents, self.control_text)
+ self.assertIsNot(None, hide_link)
+
+ def test_comment_owner_sees_hide_control(self):
+ # The comment owner sees the hide control.
+ owner = self.factory.makePerson()
+ context = self.getContext(comment_owner=owner)
+ self._test_hide_link_visible(context, owner)
+
+ def test_pillar_owner_sees_hide_control(self):
+ # The pillar owner sees the hide control.
+ person = self.factory.makePerson()
+ context = self.getContext()
+ naked_bugtask = removeSecurityProxy(context.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).owner = person
+ self._test_hide_link_visible(context, person)
+
+ def test_pillar_driver_sees_hide_control(self):
+ # The pillar driver sees the hide control.
+ person = self.factory.makePerson()
+ context = self.getContext()
+ naked_bugtask = removeSecurityProxy(context.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).driver = person
+ self._test_hide_link_visible(context, person)
+
+ def test_pillar_bug_supervisor_sees_hide_control(self):
+ # The pillar bug supervisor sees the hide control.
+ person = self.factory.makePerson()
+ context = self.getContext()
+ naked_bugtask = removeSecurityProxy(context.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).bug_supervisor = person
+ self._test_hide_link_visible(context, person)
+
+ def test_pillar_security_contact_sees_hide_control(self):
+ # The pillar security contact sees the hide control.
+ person = self.factory.makePerson()
+ context = self.getContext()
+ naked_bugtask = removeSecurityProxy(context.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).security_contact = person
+ self._test_hide_link_visible(context, person)
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2011-11-14 00:11:41 +0000
+++ lib/lp/bugs/configure.zcml 2011-11-17 23:00:32 +0000
@@ -746,6 +746,7 @@
who_made_private
date_made_private
userCanView
+ userCanSetCommentVisibility
personIsDirectSubscriber
personIsAlsoNotifiedSubscriber
personIsSubscribedToDuplicate
@@ -849,6 +850,7 @@
mute
newMessage
removeWatch
+ setCommentVisibility
setPrivate
setSecurityRelated
setPrivacyAndSecurityRelated
@@ -882,7 +884,6 @@
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2011-11-14 00:29:56 +0000
+++ lib/lp/bugs/interfaces/bug.py 2011-11-17 23:00:32 +0000
@@ -944,6 +944,25 @@
def userCanView(user):
"""Return True if `user` can see this IBug, false otherwise."""
+ def userCanSetCommentVisibility(user):
+ """Return True if `user` can set bug comment visibility.
+
+ This method is called by security adapters for authenticated users.
+
+ Users who can set bug comment visibility are:
+ - Admins and registry admins
+ - users in project roles on any bugtask:
+ - maintainer
+ - driver
+ - bug supervisor
+ - security contact
+
+ Additionally, the comment owners can hide their own comments but that
+ is not checked here - this method is to see if arbitrary users can
+ hide comments they did not make themselves.
+
+ """
+
@operation_parameters(
submission=Reference(
Interface, title=_('A HWDB submission'), required=True))
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-11-14 18:35:39 +0000
+++ lib/lp/bugs/model/bug.py 2011-11-17 23:00:32 +0000
@@ -80,6 +80,7 @@
implements,
providedBy,
)
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import (
ProxyFactory,
removeSecurityProxy,
@@ -191,6 +192,7 @@
validate_person,
validate_public_person,
)
+from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.productseries import IProductSeries
from lp.registry.interfaces.series import SeriesStatus
@@ -2081,7 +2083,16 @@
bug_message_set = getUtility(IBugMessageSet)
bug_message = bug_message_set.getByBugAndMessage(
self, self.messages[comment_number])
- bug_message.message.visible = visible
+
+ user_owns_comment = False
+ flag = 'disclosure.users_hide_own_bug_comments.enabled'
+ if bool(getFeatureFlag(flag)):
+ user_owns_comment = bug_message.owner == user
+ if (not self.userCanSetCommentVisibility(user)
+ and not user_owns_comment):
+ raise Unauthorized(
+ "User %s cannot hide or show bug comments" % user.name)
+ bug_message.message.setVisible(visible)
@cachedproperty
def _known_viewers(self):
@@ -2131,6 +2142,30 @@
return True
return False
+ def userCanSetCommentVisibility(self, user):
+ """See `IBug`"""
+
+ if user is None:
+ return False
+ roles = IPersonRoles(user)
+ if roles.in_admin or roles.in_registry_experts:
+ return True
+ flag = 'disclosure.users_hide_own_bug_comments.enabled'
+ return bool(getFeatureFlag(flag)) and self.userInProjectRole(roles)
+
+ def userInProjectRole(self, user):
+ """ Return True if user has a project role for any affected pillar."""
+ roles = IPersonRoles(user)
+ if roles is None:
+ return False
+ for pillar in self.affected_pillars:
+ if (roles.isOwner(pillar)
+ or roles.isOneOfDrivers(pillar)
+ or roles.isBugSupervisor(pillar)
+ or roles.isSecurityContact(pillar)):
+ return True
+ return False
+
def linkHWSubmission(self, submission):
"""See `IBug`."""
getUtility(IHWSubmissionBugSet).create(submission, self)
=== modified file 'lib/lp/bugs/security.py'
--- lib/lp/bugs/security.py 2011-10-26 02:00:58 +0000
+++ lib/lp/bugs/security.py 2011-11-17 23:00:32 +0000
@@ -210,15 +210,6 @@
usedfor = IMessage
-class SetBugCommentVisibility(AuthorizationBase):
- permission = 'launchpad.Admin'
- usedfor = IBug
-
- def checkAuthenticated(self, user):
- """Admins and registry admins can set bug comment visibility."""
- return (user.in_admin or user.in_registry_experts)
-
-
class ViewBugTracker(AnonymousAuthorization):
"""Anyone can view a bug tracker."""
usedfor = IBugTracker
=== modified file 'lib/lp/bugs/tests/test_bug_messages.py'
--- lib/lp/bugs/tests/test_bug_messages.py 2011-02-14 00:15:22 +0000
+++ lib/lp/bugs/tests/test_bug_messages.py 2011-11-17 23:00:32 +0000
@@ -7,7 +7,11 @@
from canonical.launchpad.ftests import login
from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.testing import TestCaseWithFactory
+from lp.services.features.testing import FeatureFixture
+from lp.testing import (
+ login_celebrity,
+ TestCaseWithFactory,
+ )
class TestBugIndexedMessages(TestCaseWithFactory):
@@ -35,3 +39,68 @@
# IIndexedMessage.
for indexed_message in self.bug_2.indexed_messages:
self.failUnlessEqual(None, indexed_message.parent)
+
+
+class TestUserCanSetCommentVisibility(TestCaseWithFactory):
+
+ """Test whether expected users can toggle bug comment visibility."""
+
+ layer = DatabaseFunctionalLayer
+
+ feature_flag = {'disclosure.users_hide_own_bug_comments.enabled': 'on'}
+
+ def test_random_user_cannot_toggle_comment_visibility(self):
+ # A random user cannot set bug comment visibility.
+ person = self.factory.makePerson()
+ bug = self.factory.makeBug()
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+ with FeatureFixture(self.feature_flag):
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+
+ def test_registry_admin_can_toggle_comment_visibility(self):
+ # Members of registry experts can set bug comment visibility.
+ person = login_celebrity('registry_experts')
+ bug = self.factory.makeBug()
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
+
+ def test_admin_can_toggle_comment_visibility(self):
+ # Admins can set bug comment visibility.
+ person = login_celebrity('admin')
+ bug = self.factory.makeBug()
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
+
+ def test_pillar_owner_can_toggle_comment_visibility(self):
+ # Pillar owner can set bug comment visibility.
+ person = self.factory.makePerson()
+ product = self.factory.makeProduct(owner=person)
+ bug = self.factory.makeBug(product=product)
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+ with FeatureFixture(self.feature_flag):
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
+
+ def test_pillar_driver_can_toggle_comment_visibility(self):
+ # Pillar driver can set bug comment visibility.
+ person = self.factory.makePerson()
+ product = self.factory.makeProduct(driver=person)
+ bug = self.factory.makeBug(product=product)
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+ with FeatureFixture(self.feature_flag):
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
+
+ def test_pillar_bug_supervisor_can_toggle_comment_visibility(self):
+ # Pillar bug supervisor can set bug comment visibility.
+ person = self.factory.makePerson()
+ product = self.factory.makeProduct(bug_supervisor=person)
+ bug = self.factory.makeBug(product=product)
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+ with FeatureFixture(self.feature_flag):
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
+
+ def test_pillar_security_contact_can_toggle_comment_visibility(self):
+ # Pillar security contact can set bug comment visibility.
+ person = self.factory.makePerson()
+ product = self.factory.makeProduct(security_contact=person)
+ bug = self.factory.makeBug(product=product)
+ self.assertFalse(bug.userCanSetCommentVisibility(person))
+ with FeatureFixture(self.feature_flag):
+ self.assertTrue(bug.userCanSetCommentVisibility(person))
=== modified file 'lib/lp/bugs/tests/test_bug_messages_webservice.py'
--- lib/lp/bugs/tests/test_bug_messages_webservice.py 2011-04-05 22:34:35 +0000
+++ lib/lp/bugs/tests/test_bug_messages_webservice.py 2011-11-17 23:00:32 +0000
@@ -2,6 +2,7 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Webservice unit tests related to Launchpad Bug messages."""
+from lp.services.features.testing import FeatureFixture
__metaclass__ = type
@@ -10,15 +11,14 @@
from lazr.restfulclient.errors import HTTPError
from zope.component import getUtility
from zope.security.management import endInteraction
+from zope.security.proxy import removeSecurityProxy
-from canonical.testing.layers import (
- DatabaseFunctionalLayer,
- LaunchpadFunctionalLayer,
- )
+from canonical.testing.layers import DatabaseFunctionalLayer
from lp.bugs.interfaces.bugmessage import IBugMessageSet
from lp.registry.interfaces.person import IPersonSet
from lp.testing import (
launchpadlib_for,
+ login_celebrity,
person_logged_in,
TestCaseWithFactory,
WebServiceTestCase,
@@ -56,6 +56,8 @@
layer = DatabaseFunctionalLayer
+ feature_flag = {'disclosure.users_hide_own_bug_comments.enabled': 'on'}
+
def setUp(self):
super(TestSetCommentVisibility, self).setUp()
self.person_set = getUtility(IPersonSet)
@@ -115,10 +117,7 @@
def test_registry_admin_can_set_visible(self):
# Members of registry experts can set bug comment
# visibility.
- registry = self.person_set.getByName('registry')
- person = self.factory.makePerson()
- with person_logged_in(registry.teamowner):
- registry.addMember(person, registry.teamowner)
+ person = login_celebrity('registry_experts')
bug = self._get_bug_for_user(person)
self._set_visibility(bug)
self.assertCommentHidden()
@@ -126,10 +125,51 @@
def test_admin_can_set_visible(self):
# Admins can set bug comment
# visibility.
- admins = self.person_set.getByName('admins')
- person = self.factory.makePerson()
- with person_logged_in(admins.teamowner):
- admins.addMember(person, admins.teamowner)
+ person = login_celebrity('admin')
bug = self._get_bug_for_user(person)
self._set_visibility(bug)
self.assertCommentHidden()
+
+ def _test_hide_comment_with_feature_flag(self, person):
+ bug = self._get_bug_for_user(person)
+ self.assertRaises(
+ HTTPError,
+ self._set_visibility,
+ bug)
+ with FeatureFixture(self.feature_flag):
+ self._set_visibility(bug)
+ self.assertCommentHidden()
+
+ def test_pillar_owner_can_set_visible(self):
+ # Pillar owner can set bug comment visibility.
+ person = self.factory.makePerson()
+ naked_bugtask = removeSecurityProxy(self.bug.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).owner = person
+ self._test_hide_comment_with_feature_flag(person)
+
+ def test_pillar_driver_can_set_visible(self):
+ # Pillar driver can set bug comment visibility.
+ person = self.factory.makePerson()
+ naked_bugtask = removeSecurityProxy(self.bug.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).driver = person
+ self._test_hide_comment_with_feature_flag(person)
+
+ def test_pillar_bug_supervisor_can_set_visible(self):
+ # Pillar bug supervisor can set bug comment visibility.
+ person = self.factory.makePerson()
+ naked_bugtask = removeSecurityProxy(self.bug.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).bug_supervisor = person
+ self._test_hide_comment_with_feature_flag(person)
+
+ def test_pillar_security_contact_can_set_visible(self):
+ # Pillar security_contact can set bug comment visibility.
+ person = self.factory.makePerson()
+ naked_bugtask = removeSecurityProxy(self.bug.default_bugtask)
+ removeSecurityProxy(naked_bugtask.pillar).security_contact = person
+ self._test_hide_comment_with_feature_flag(person)
+
+ def test_comment_owner_can_set_visible(self):
+ # The author of the comment can set bug comment visibility.
+ person = self.factory.makePerson()
+ removeSecurityProxy(self.message).owner = person
+ self._test_hide_comment_with_feature_flag(person)
=== modified file 'lib/lp/coop/answersbugs/visibility.py'
--- lib/lp/coop/answersbugs/visibility.py 2011-05-17 13:36:44 +0000
+++ lib/lp/coop/answersbugs/visibility.py 2011-11-17 23:00:32 +0000
@@ -61,7 +61,7 @@
control_text = 'mark-spam-1'
- def getContext(self):
+ def getContext(self, comment_owner=None):
"""To be overwridden by subclasses.
This method must create and return a message bearing object
=== modified file 'lib/lp/registry/interfaces/role.py'
--- lib/lp/registry/interfaces/role.py 2011-11-10 01:13:44 +0000
+++ lib/lp/registry/interfaces/role.py 2011-11-17 23:00:32 +0000
@@ -128,6 +128,12 @@
def isDriver(obj):
"""Is this person the driver of the object?"""
+ def isBugSupervisor(obj):
+ """Is this person the bug supervisor of the object?"""
+
+ def isSecurityContact(obj):
+ """Is this person the security contact of the object?"""
+
def isOneOfDrivers(obj):
"""Is this person on of the drivers of the object?
=== modified file 'lib/lp/registry/model/personroles.py'
--- lib/lp/registry/model/personroles.py 2011-11-10 01:13:44 +0000
+++ lib/lp/registry/model/personroles.py 2011-11-17 23:00:32 +0000
@@ -13,6 +13,8 @@
from zope.interface import implements
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
+from lp.bugs.interfaces.securitycontact import IHasSecurityContact
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.role import (
IHasDrivers,
@@ -49,6 +51,16 @@
"""See IPersonRoles."""
return self.person.inTeam(obj.owner)
+ def isBugSupervisor(self, obj):
+ """See IPersonRoles."""
+ return (IHasBugSupervisor.providedBy(obj)
+ and self.person.inTeam(obj.bug_supervisor))
+
+ def isSecurityContact(self, obj):
+ """See IPersonRoles."""
+ return (IHasSecurityContact.providedBy(obj)
+ and self.person.inTeam(obj.security_contact))
+
def isDriver(self, obj):
"""See IPersonRoles."""
return self.person.inTeam(obj.driver)
=== modified file 'lib/lp/registry/tests/test_personroles.py'
--- lib/lp/registry/tests/test_personroles.py 2011-11-10 01:13:44 +0000
+++ lib/lp/registry/tests/test_personroles.py 2011-11-17 23:00:32 +0000
@@ -115,6 +115,18 @@
roles = IPersonRoles(self.person)
self.assertFalse(roles.isOneOfDrivers(sprint))
+ def test_isBugSupervisor(self):
+ # The person can be the bug supervisor of something, e.g. a product.
+ product = self.factory.makeProduct(bug_supervisor=self.person)
+ roles = IPersonRoles(self.person)
+ self.assertTrue(roles.isBugSupervisor(product))
+
+ def test_isSecurityContact(self):
+ # The person can be the security contact of something, e.g. a product.
+ product = self.factory.makeProduct(security_contact=self.person)
+ roles = IPersonRoles(self.person)
+ self.assertTrue(roles.isSecurityContact(product))
+
def test_isOneOf(self):
# Objects may have multiple roles that a person can fulfill.
# Specifications are such a case.
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2011-11-12 10:20:55 +0000
+++ lib/lp/services/features/flags.py 2011-11-17 23:00:32 +0000
@@ -194,6 +194,12 @@
'',
'',
''),
+ ('disclosure.users_hide_own_bug_comments.enabled',
+ 'boolean',
+ 'Allows users in project roles and comment owners to hide bug comments.',
+ '',
+ '',
+ ''),
('bugs.autoconfirm.enabled_distribution_names',
'space delimited',
('Enables auto-confirming bugtasks for distributions (and their '
=== modified file 'lib/lp/services/messages/configure.zcml'
--- lib/lp/services/messages/configure.zcml 2011-05-13 16:08:03 +0000
+++ lib/lp/services/messages/configure.zcml 2011-11-17 23:00:32 +0000
@@ -12,6 +12,12 @@
+
+
=== modified file 'lib/lp/services/messages/model/message.py'
--- lib/lp/services/messages/model/message.py 2011-08-15 19:14:22 +0000
+++ lib/lp/services/messages/model/message.py 2011-11-17 23:00:32 +0000
@@ -139,6 +139,9 @@
"""See IMessage.__iter__"""
return iter(self.chunks)
+ def setVisible(self, visible):
+ self.visible = visible
+
@property
def followup_title(self):
"""See IMessage."""