Merge lp:~gmb/launchpad/team-subscription-opt-out-apis into lp:launchpad/db-devel

Proposed by Graham Binns
Status: Merged
Merged at revision: 10392
Proposed branch: lp:~gmb/launchpad/team-subscription-opt-out-apis
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~gmb/launchpad/team-subscription-opt-out
Diff against target: 285 lines (+193/-5)
4 files modified
lib/lp/bugs/configure.zcml (+3/-1)
lib/lp/bugs/interfaces/bugsubscriptionfilter.py (+41/-3)
lib/lp/bugs/model/bugsubscriptionfilter.py (+39/-0)
lib/lp/bugs/tests/test_structuralsubscription.py (+110/-1)
To merge this branch: bzr merge lp:~gmb/launchpad/team-subscription-opt-out-apis
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+55982@code.launchpad.net

Commit message

[r=bac][ui=none][bug=751173] APIs have been added to IBugSubscriptionFilter to allow for muting and unmuting of subscriptions. These have also been exposed in the devel API.

Description of the change

This branch adds APIs around the BugSubscriptionFilterMute model. The APIs added are:

 - BugSubscriptionFilter.isMuteAllowed(person):
   Returns True if `person` can add a mute for the current filter.
 - BugSubscriptionFilter.mute(person):
   Add a mute for `person` and return it. If called for a user who is already
   muted, return the existing mute. Raise an error if isMuteAllowed() would
   return False for `person`.
 - BugSubscriptionFilter.unmute(person):
   Remove any mutes for `person` on the current Filter.

I've added tests to cover these APIs.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Graham this branch looks great and is quite thorough. I like your idempotent tests.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2011-04-04 12:34:00 +0000
+++ lib/lp/bugs/configure.zcml 2011-04-04 12:34:01 +0000
@@ -636,9 +636,11 @@
636 class=".model.bugsubscriptionfilter.BugSubscriptionFilter">636 class=".model.bugsubscriptionfilter.BugSubscriptionFilter">
637 <allow637 <allow
638 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes"/>638 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes"/>
639 <allow
640 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterMethodsPublic"/>
639 <require641 <require
640 permission="launchpad.Edit"642 permission="launchpad.Edit"
641 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterMethods"643 interface=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterMethodsProtected"
642 set_schema=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes" />644 set_schema=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes" />
643 </class>645 </class>
644646
645647
=== modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py'
--- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-04 12:34:00 +0000
+++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-04 12:34:01 +0000
@@ -11,9 +11,15 @@
1111
1212
13from lazr.restful.declarations import (13from lazr.restful.declarations import (
14 call_with,
14 export_as_webservice_entry,15 export_as_webservice_entry,
15 export_destructor_operation,16 export_destructor_operation,
17 export_read_operation,
18 export_write_operation,
16 exported,19 exported,
20 operation_for_version,
21 operation_parameters,
22 REQUEST_USER,
17 )23 )
18from lazr.restful.fields import Reference24from lazr.restful.fields import Reference
19from zope.interface import Interface25from zope.interface import Interface
@@ -35,6 +41,7 @@
35from lp.bugs.interfaces.structuralsubscription import (41from lp.bugs.interfaces.structuralsubscription import (
36 IStructuralSubscription,42 IStructuralSubscription,
37 )43 )
44from lp.registry.interfaces.person import IPerson
38from lp.services.fields import (45from lp.services.fields import (
39 PersonChoice,46 PersonChoice,
40 SearchTag,47 SearchTag,
@@ -99,8 +106,36 @@
99 value_type=SearchTag()))106 value_type=SearchTag()))
100107
101108
102class IBugSubscriptionFilterMethods(Interface):109class IBugSubscriptionFilterMethodsPublic(Interface):
103 """Methods of `IBugSubscriptionFilter`."""110 """Methods on `IBugSubscriptionFilter` that can be called by anyone."""
111
112 @call_with(person=REQUEST_USER)
113 @operation_parameters(
114 person=Reference(IPerson, title=_('Person'), required=True))
115 @export_read_operation()
116 @operation_for_version('devel')
117 def isMuteAllowed(person):
118 """Return True if this filter can be muted for `person`."""
119
120 @call_with(person=REQUEST_USER)
121 @operation_parameters(
122 person=Reference(IPerson, title=_('Person'), required=True))
123 @export_write_operation()
124 @operation_for_version('devel')
125 def mute(person):
126 """Add a mute for `person` to this filter."""
127
128 @call_with(person=REQUEST_USER)
129 @operation_parameters(
130 person=Reference(IPerson, title=_('Person'), required=True))
131 @export_write_operation()
132 @operation_for_version('devel')
133 def unmute(person):
134 """Remove any mute for `person` to this filter."""
135
136
137class IBugSubscriptionFilterMethodsProtected(Interface):
138 """Methods of `IBugSubscriptionFilter` that require launchpad.Edit."""
104139
105 @export_destructor_operation()140 @export_destructor_operation()
106 def delete():141 def delete():
@@ -111,7 +146,8 @@
111146
112147
113class IBugSubscriptionFilter(148class IBugSubscriptionFilter(
114 IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods):149 IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethodsProtected,
150 IBugSubscriptionFilterMethodsPublic):
115 """A bug subscription filter."""151 """A bug subscription filter."""
116 export_as_webservice_entry()152 export_as_webservice_entry()
117153
@@ -119,6 +155,8 @@
119class IBugSubscriptionFilterMute(Interface):155class IBugSubscriptionFilterMute(Interface):
120 """A mute on an IBugSubscriptionFilter."""156 """A mute on an IBugSubscriptionFilter."""
121157
158 export_as_webservice_entry()
159
122 person = PersonChoice(160 person = PersonChoice(
123 title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',161 title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',
124 readonly=True, description=_("The person subscribed."))162 readonly=True, description=_("The person subscribed."))
125163
=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-04 12:34:00 +0000
+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-04 12:34:01 +0000
@@ -49,6 +49,10 @@
49from lp.services.database.stormbase import StormBase49from lp.services.database.stormbase import StormBase
5050
5151
52class MuteNotAllowed(Exception):
53 """Raised when someone tries to mute a filter that can't be muted."""
54
55
52class BugSubscriptionFilter(StormBase):56class BugSubscriptionFilter(StormBase):
53 """A filter to specialize a *structural* subscription."""57 """A filter to specialize a *structural* subscription."""
5458
@@ -249,6 +253,41 @@
249 # subscription.253 # subscription.
250 self.structural_subscription.delete()254 self.structural_subscription.delete()
251255
256 def isMuteAllowed(self, person):
257 """See `IBugSubscriptionFilter`."""
258 return (
259 self.structural_subscription.subscriber.isTeam() and
260 person.inTeam(self.structural_subscription.subscriber))
261
262 def mute(self, person):
263 """See `IBugSubscriptionFilter`."""
264 if not self.isMuteAllowed(person):
265 raise MuteNotAllowed(
266 "This subscription cannot be muted for %s" % person.name)
267
268 store = Store.of(self)
269 existing_mutes = store.find(
270 BugSubscriptionFilterMute,
271 BugSubscriptionFilterMute.filter_id == self.id,
272 BugSubscriptionFilterMute.person_id == person.id)
273 if not existing_mutes.is_empty():
274 return existing_mutes.one()
275 else:
276 mute = BugSubscriptionFilterMute()
277 mute.person = person
278 mute.filter = self.id
279 store.add(mute)
280 return mute
281
282 def unmute(self, person):
283 """See `IBugSubscriptionFilter`."""
284 store = Store.of(self)
285 existing_mutes = store.find(
286 BugSubscriptionFilterMute,
287 BugSubscriptionFilterMute.filter_id == self.id,
288 BugSubscriptionFilterMute.person_id == person.id)
289 existing_mutes.remove()
290
252291
253class BugSubscriptionFilterMute(StormBase):292class BugSubscriptionFilterMute(StormBase):
254 """A filter to specialize a *structural* subscription."""293 """A filter to specialize a *structural* subscription."""
255294
=== modified file 'lib/lp/bugs/tests/test_structuralsubscription.py'
--- lib/lp/bugs/tests/test_structuralsubscription.py 2011-03-21 18:23:31 +0000
+++ lib/lp/bugs/tests/test_structuralsubscription.py 2011-04-04 12:34:01 +0000
@@ -23,7 +23,11 @@
23 BugTaskStatus,23 BugTaskStatus,
24 )24 )
25from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients25from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
26from lp.bugs.model.bugsubscriptionfilter import BugSubscriptionFilter26from lp.bugs.model.bugsubscriptionfilter import (
27 BugSubscriptionFilter,
28 BugSubscriptionFilterMute,
29 MuteNotAllowed,
30 )
27from lp.bugs.model.structuralsubscription import (31from lp.bugs.model.structuralsubscription import (
28 get_structural_subscriptions_for_bug,32 get_structural_subscriptions_for_bug,
29 get_structural_subscribers,33 get_structural_subscribers,
@@ -680,3 +684,108 @@
680 [], list(684 [], list(
681 get_structural_subscribers(685 get_structural_subscribers(
682 bug, None, BugNotificationLevel.COMMENTS, None)))686 bug, None, BugNotificationLevel.COMMENTS, None)))
687
688
689class TestBugSubscriptionFilterMute(TestCaseWithFactory):
690 """Tests for the BugSubscriptionFilterMute class."""
691
692 layer = DatabaseFunctionalLayer
693
694 def setUp(self):
695 super(TestBugSubscriptionFilterMute, self).setUp()
696 self.target = self.factory.makeProduct()
697 self.team = self.factory.makeTeam()
698 self.team_member = self.factory.makePerson()
699 with person_logged_in(self.team.teamowner):
700 self.team.addMember(self.team_member, self.team.teamowner)
701 self.team_subscription = self.target.addBugSubscription(
702 self.team, self.team.teamowner)
703 self.filter = self.team_subscription.bug_filters.one()
704
705 def test_isMuteAllowed_returns_true_for_team_subscriptions(self):
706 # BugSubscriptionFilter.isMuteAllowed() will return True for
707 # subscriptions where the owner of the subscription is a team.
708 self.assertTrue(self.filter.isMuteAllowed(self.team_member))
709
710 def test_isMuteAllowed_returns_false_for_non_team_subscriptions(self):
711 # BugSubscriptionFilter.isMuteAllowed() will return False for
712 # subscriptions where the owner of the subscription is not a team.
713 person = self.factory.makePerson()
714 with person_logged_in(person):
715 non_team_subscription = self.target.addBugSubscription(
716 person, person)
717 filter = non_team_subscription.bug_filters.one()
718 self.assertFalse(filter.isMuteAllowed(person))
719
720 def test_isMuteAllowed_returns_false_for_non_team_members(self):
721 # BugSubscriptionFilter.isMuteAllowed() will return False if the
722 # user passed to it is not a member of the subscribing team.
723 non_team_person = self.factory.makePerson()
724 self.assertFalse(self.filter.isMuteAllowed(non_team_person))
725
726 def test_mute_adds_mute(self):
727 # BugSubscriptionFilter.mute() adds a mute for the filter.
728 filter_id = self.filter.id
729 person_id = self.team_member.id
730 store = Store.of(self.filter)
731 mutes = store.find(
732 BugSubscriptionFilterMute,
733 BugSubscriptionFilterMute.filter == filter_id,
734 BugSubscriptionFilterMute.person == person_id)
735 self.assertTrue(mutes.is_empty())
736 self.filter.mute(self.team_member)
737 store.flush()
738
739 def test_unmute_removes_mute(self):
740 # BugSubscriptionFilter.unmute() removes any mute for a given
741 # person on that filter.
742 filter_id = self.filter.id
743 person_id = self.team_member.id
744 store = Store.of(self.filter)
745 self.filter.mute(self.team_member)
746 store.flush()
747 mutes = store.find(
748 BugSubscriptionFilterMute,
749 BugSubscriptionFilterMute.filter == filter_id,
750 BugSubscriptionFilterMute.person == person_id)
751 self.assertFalse(mutes.is_empty())
752 self.filter.unmute(self.team_member)
753 store.flush()
754 self.assertTrue(mutes.is_empty())
755
756 def test_mute_is_idempotent(self):
757 # Muting works even if the user is already muted.
758 store = Store.of(self.filter)
759 mute = self.filter.mute(self.team_member)
760 store.flush()
761 second_mute = self.filter.mute(self.team_member)
762 self.assertEqual(mute, second_mute)
763
764 def test_unmute_is_idempotent(self):
765 # Unmuting works even if the user is not muted
766 store = Store.of(self.filter)
767 mutes = store.find(
768 BugSubscriptionFilterMute,
769 BugSubscriptionFilterMute.filter == self.filter.id,
770 BugSubscriptionFilterMute.person == self.team_member.id)
771 self.assertTrue(mutes.is_empty())
772 self.filter.unmute(self.team_member)
773 self.assertTrue(mutes.is_empty())
774
775 def test_mute_raises_error_for_non_team_subscriptions(self):
776 # BugSubscriptionFilter.mute() will raise an error if called on
777 # a non-team subscription.
778 person = self.factory.makePerson()
779 with person_logged_in(person):
780 non_team_subscription = self.target.addBugSubscription(
781 person, person)
782 filter = non_team_subscription.bug_filters.one()
783 self.assertFalse(filter.isMuteAllowed(person))
784 self.assertRaises(MuteNotAllowed, filter.mute, person)
785
786 def test_mute_raises_error_for_non_team_members(self):
787 # BugSubscriptionFilter.mute() will raise an error if called on
788 # a subscription of which the calling person is not a member.
789 non_team_person = self.factory.makePerson()
790 self.assertFalse(self.filter.isMuteAllowed(non_team_person))
791 self.assertRaises(MuteNotAllowed, self.filter.mute, non_team_person)

Subscribers

People subscribed via source and target branches

to status/vote changes: