Merge lp:~danilo/launchpad/bug-772763-remove-unmute-dialog-part1 into lp:launchpad/db-devel
- bug-772763-remove-unmute-dialog-part1
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Данило Шеган |
Approved revision: | no longer in the source branch. |
Merged at revision: | 10582 |
Proposed branch: | lp:~danilo/launchpad/bug-772763-remove-unmute-dialog-part1 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
518 lines (+204/-54) 9 files modified
database/schema/security.cfg (+4/-0) lib/lp/bugs/browser/bug.py (+2/-4) lib/lp/bugs/browser/bugsubscription.py (+16/-7) lib/lp/bugs/browser/tests/test_bug_views.py (+24/-1) lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+46/-13) lib/lp/bugs/interfaces/bug.py (+5/-3) lib/lp/bugs/model/bug.py (+15/-5) lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+76/-21) lib/lp/bugs/tests/test_bug.py (+16/-0) |
To merge this branch: | bzr merge lp:~danilo/launchpad/bug-772763-remove-unmute-dialog-part1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Данило Шеган (community) | db | Abstain | |
Review via email: mp+61775@code.launchpad.net |
Commit message
[r=adeuring][bug=772763][incr] Ground-work for making "Unmute bug mail" unmute directly (and restoring any previous subscription) without popping up a dialog.
Description of the change
= Bug 772763: remove unmute dialog, part1 =
As part of solving 772763, we remove the pop-up unmute dialog (to directly unmute instead). To prepare for that, we first do some server-side cleanups and make unmute() method return the previously masked subscription (if any).
== Proposed fix ==
This branch does a few things:
- Make "static" (i.e. non-JS) page IBugTask:+mute not redirect to +subscribe to offer unmute but instead allow direct unmuting
- Ensures Mute/Unmute link is shown when bug is muted
- Takes mutes into consideration for all subscribers-
- Make unmute() method return a previous subscription (if any)
== Pre-implementation notes ==
This is mostly Gary's branch. I am only shepherding it and providing a few tests of my own.
== Implementation details ==
Not cleaning up the lint for the model/bug.py since it'd taint the diff.
== Tests ==
bin/test -cvvt TestBugSubscrip
== Demo and Q/A ==
Check that IBugTask:+mute page behaves as expected (iow, allows unmuting as well). Rest of the QA will be possible only when the follow-up branch lands.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/lp/
575: E225 missing whitespace around operator
747: E225 missing whitespace around operator
751: E225 missing whitespace around operator
766: E225 missing whitespace around operator
1461: E225 missing whitespace around operator
1674: E261 at least two spaces before inline comment
1676: E261 at least two spaces before inline comment
1687: E261 at least two spaces before inline comment
1689: E261 at least two spaces before inline comment
1707: E225 missing whitespace around operator
2262: E225 missing whitespace around operator
2277: E225 missing whitespace around operator
2287: E261 at least two spaces before inline comment
2325: E225 missing whitespace around operator
2587: E225 missing whitespace around operator
2628: E225 missing whitespace around operator
Данило Шеган (danilo) : | # |
Abel Deuring (adeuring) : | # |
Preview Diff
1 | === modified file 'database/schema/security.cfg' | |||
2 | --- database/schema/security.cfg 2011-05-20 16:15:35 +0000 | |||
3 | +++ database/schema/security.cfg 2011-05-23 09:01:25 +0000 | |||
4 | @@ -546,6 +546,7 @@ | |||
5 | 546 | public.bugcve = SELECT, INSERT | 546 | public.bugcve = SELECT, INSERT |
6 | 547 | public.bugjob = SELECT, INSERT | 547 | public.bugjob = SELECT, INSERT |
7 | 548 | public.bugmessage = SELECT, INSERT, UPDATE | 548 | public.bugmessage = SELECT, INSERT, UPDATE |
8 | 549 | public.bugmute = SELECT | ||
9 | 549 | public.bugnomination = SELECT | 550 | public.bugnomination = SELECT |
10 | 550 | public.bugnotification = SELECT, INSERT | 551 | public.bugnotification = SELECT, INSERT |
11 | 551 | public.bugnotificationfilter = SELECT, INSERT | 552 | public.bugnotificationfilter = SELECT, INSERT |
12 | @@ -1238,6 +1239,7 @@ | |||
13 | 1238 | public.bugcve = SELECT, INSERT | 1239 | public.bugcve = SELECT, INSERT |
14 | 1239 | public.bugjob = SELECT, INSERT | 1240 | public.bugjob = SELECT, INSERT |
15 | 1240 | public.bugmessage = SELECT, INSERT | 1241 | public.bugmessage = SELECT, INSERT |
16 | 1242 | public.bugmute = SELECT | ||
17 | 1241 | public.bugnomination = SELECT | 1243 | public.bugnomination = SELECT |
18 | 1242 | public.bugnotification = SELECT, INSERT | 1244 | public.bugnotification = SELECT, INSERT |
19 | 1243 | public.bugnotificationfilter = SELECT, INSERT | 1245 | public.bugnotificationfilter = SELECT, INSERT |
20 | @@ -1341,6 +1343,7 @@ | |||
21 | 1341 | public.bugcve = SELECT, INSERT | 1343 | public.bugcve = SELECT, INSERT |
22 | 1342 | public.bugjob = SELECT, INSERT | 1344 | public.bugjob = SELECT, INSERT |
23 | 1343 | public.bugmessage = SELECT, INSERT | 1345 | public.bugmessage = SELECT, INSERT |
24 | 1346 | public.bugmute = SELECT | ||
25 | 1344 | public.bugnomination = SELECT | 1347 | public.bugnomination = SELECT |
26 | 1345 | public.bugnotification = SELECT, INSERT | 1348 | public.bugnotification = SELECT, INSERT |
27 | 1346 | public.bugnotificationfilter = SELECT, INSERT | 1349 | public.bugnotificationfilter = SELECT, INSERT |
28 | @@ -1638,6 +1641,7 @@ | |||
29 | 1638 | public.bugcve = SELECT, INSERT | 1641 | public.bugcve = SELECT, INSERT |
30 | 1639 | public.bugjob = SELECT, INSERT | 1642 | public.bugjob = SELECT, INSERT |
31 | 1640 | public.bugmessage = SELECT, INSERT | 1643 | public.bugmessage = SELECT, INSERT |
32 | 1644 | public.bugmute = SELECT | ||
33 | 1641 | public.bugnomination = SELECT, INSERT, UPDATE | 1645 | public.bugnomination = SELECT, INSERT, UPDATE |
34 | 1642 | public.bugnotification = SELECT, INSERT | 1646 | public.bugnotification = SELECT, INSERT |
35 | 1643 | public.bugnotificationattachment = SELECT | 1647 | public.bugnotificationattachment = SELECT |
36 | 1644 | 1648 | ||
37 | === modified file 'lib/lp/bugs/browser/bug.py' | |||
38 | --- lib/lp/bugs/browser/bug.py 2011-05-17 14:54:56 +0000 | |||
39 | +++ lib/lp/bugs/browser/bug.py 2011-05-23 09:01:25 +0000 | |||
40 | @@ -292,7 +292,7 @@ | |||
41 | 292 | 292 | ||
42 | 293 | return Link( | 293 | return Link( |
43 | 294 | link, text, icon='remove', summary=( | 294 | link, text, icon='remove', summary=( |
45 | 295 | "Mute this bug so that you will never receive emails " | 295 | "Mute this bug so that you will not receive emails " |
46 | 296 | "about it.")) | 296 | "about it.")) |
47 | 297 | 297 | ||
48 | 298 | def nominate(self): | 298 | def nominate(self): |
49 | @@ -538,9 +538,7 @@ | |||
50 | 538 | """Return True if the user should see the Mute link.""" | 538 | """Return True if the user should see the Mute link.""" |
51 | 539 | if features.getFeatureFlag('malone.advanced-subscriptions.enabled'): | 539 | if features.getFeatureFlag('malone.advanced-subscriptions.enabled'): |
52 | 540 | user_is_subscribed = ( | 540 | user_is_subscribed = ( |
56 | 541 | # Note that we don't have to check for isMuted(), since | 541 | self.context.isMuted(self.user) or |
54 | 542 | # if isMuted() is True isSubscribed() will also be | ||
55 | 543 | # True. | ||
57 | 544 | self.context.isSubscribed(self.user) or | 542 | self.context.isSubscribed(self.user) or |
58 | 545 | self.context.isSubscribedToDupes(self.user) or | 543 | self.context.isSubscribedToDupes(self.user) or |
59 | 546 | self.context.personIsAlsoNotifiedSubscriber(self.user)) | 544 | self.context.personIsAlsoNotifiedSubscriber(self.user)) |
60 | 547 | 545 | ||
61 | === modified file 'lib/lp/bugs/browser/bugsubscription.py' | |||
62 | --- lib/lp/bugs/browser/bugsubscription.py 2011-05-18 08:17:35 +0000 | |||
63 | +++ lib/lp/bugs/browser/bugsubscription.py 2011-05-23 09:01:25 +0000 | |||
64 | @@ -630,7 +630,10 @@ | |||
65 | 630 | 630 | ||
66 | 631 | @property | 631 | @property |
67 | 632 | def label(self): | 632 | def label(self): |
69 | 633 | return "Mute bug mail for bug %s" % self.context.bug.id | 633 | if self.context.bug.isMuted(self.user): |
70 | 634 | return "Unmute bug mail for bug %s" % self.context.bug.id | ||
71 | 635 | else: | ||
72 | 636 | return "Mute bug mail for bug %s" % self.context.bug.id | ||
73 | 634 | 637 | ||
74 | 635 | page_title = label | 638 | page_title = label |
75 | 636 | 639 | ||
76 | @@ -641,15 +644,21 @@ | |||
77 | 641 | cancel_url = next_url | 644 | cancel_url = next_url |
78 | 642 | 645 | ||
79 | 643 | def initialize(self): | 646 | def initialize(self): |
80 | 647 | self.is_muted = self.context.bug.isMuted(self.user) | ||
81 | 644 | super(BugMuteSelfView, self).initialize() | 648 | super(BugMuteSelfView, self).initialize() |
82 | 645 | # If the user is already muted, redirect them to the +subscribe | ||
83 | 646 | # page, since there's no point doing its work twice. | ||
84 | 647 | if self.context.bug.isMuted(self.user): | ||
85 | 648 | self.request.response.redirect( | ||
86 | 649 | canonical_url(self.context, view_name="+subscribe")) | ||
87 | 650 | 649 | ||
89 | 651 | @action('Mute bug mail', name='mute') | 650 | @action('Mute bug mail', |
90 | 651 | name='mute', | ||
91 | 652 | condition=lambda form, action: not form.is_muted) | ||
92 | 652 | def mute_action(self, action, data): | 653 | def mute_action(self, action, data): |
93 | 653 | self.context.bug.mute(self.user, self.user) | 654 | self.context.bug.mute(self.user, self.user) |
94 | 654 | self.request.response.addInfoNotification( | 655 | self.request.response.addInfoNotification( |
95 | 655 | "Mail for bug #%s has been muted." % self.context.bug.id) | 656 | "Mail for bug #%s has been muted." % self.context.bug.id) |
96 | 657 | |||
97 | 658 | @action('Unmute bug mail', | ||
98 | 659 | name='unmute', | ||
99 | 660 | condition=lambda form, action: form.is_muted) | ||
100 | 661 | def unmute_action(self, action, data): | ||
101 | 662 | self.context.bug.unmute(self.user, self.user) | ||
102 | 663 | self.request.response.addInfoNotification( | ||
103 | 664 | "Mail for bug #%s has been unmuted." % self.context.bug.id) | ||
104 | 656 | 665 | ||
105 | === modified file 'lib/lp/bugs/browser/tests/test_bug_views.py' | |||
106 | --- lib/lp/bugs/browser/tests/test_bug_views.py 2011-05-17 14:54:56 +0000 | |||
107 | +++ lib/lp/bugs/browser/tests/test_bug_views.py 2011-05-23 09:01:25 +0000 | |||
108 | @@ -173,7 +173,30 @@ | |||
109 | 173 | self.assertTrue('mute_subscription' in html) | 173 | self.assertTrue('mute_subscription' in html) |
110 | 174 | # The template uses user_should_see_mute_link to decide | 174 | # The template uses user_should_see_mute_link to decide |
111 | 175 | # whether or not to display the mute link. | 175 | # whether or not to display the mute link. |
112 | 176 | soup = BeautifulSoup(html) | ||
113 | 177 | self.assertTrue( | 176 | self.assertTrue( |
114 | 178 | self._hasCSSClass(html, 'mute-link-container', 'hidden'), | 177 | self._hasCSSClass(html, 'mute-link-container', 'hidden'), |
115 | 179 | 'No "hidden" CSS class in mute-link-container.') | 178 | 'No "hidden" CSS class in mute-link-container.') |
116 | 179 | |||
117 | 180 | def test_mute_subscription_link_shown_if_muted(self): | ||
118 | 181 | # If a person is muted but not otherwise subscribed, they should still | ||
119 | 182 | # see the (un)mute link. | ||
120 | 183 | person = self.factory.makePerson() | ||
121 | 184 | with person_logged_in(person): | ||
122 | 185 | with FeatureFixture({self.feature_flag_1: 'on'}): | ||
123 | 186 | self.bug.mute(person, person) | ||
124 | 187 | # The user isn't subscribed already, but is muted. | ||
125 | 188 | self.assertFalse(self.bug.isSubscribed(person)) | ||
126 | 189 | self.assertFalse( | ||
127 | 190 | self.bug.personIsAlsoNotifiedSubscriber( | ||
128 | 191 | person)) | ||
129 | 192 | self.assertTrue(self.bug.isMuted(person)) | ||
130 | 193 | view = create_initialized_view( | ||
131 | 194 | self.bug, name="+portlet-subscribers") | ||
132 | 195 | self.assertTrue(view.user_should_see_mute_link, | ||
133 | 196 | "User should see mute link.") | ||
134 | 197 | contents = view.render() | ||
135 | 198 | self.assertTrue('mute_subscription' in contents, | ||
136 | 199 | "'mute_subscription' not in contents.") | ||
137 | 200 | self.assertFalse( | ||
138 | 201 | self._hasCSSClass( | ||
139 | 202 | contents, 'mute-link-container', 'hidden')) | ||
140 | 180 | 203 | ||
141 | === modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py' | |||
142 | --- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-05-17 11:44:33 +0000 | |||
143 | +++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-05-23 09:01:25 +0000 | |||
144 | @@ -8,7 +8,6 @@ | |||
145 | 8 | import transaction | 8 | import transaction |
146 | 9 | 9 | ||
147 | 10 | from canonical.launchpad.ftests import LaunchpadFormHarness | 10 | from canonical.launchpad.ftests import LaunchpadFormHarness |
148 | 11 | from canonical.launchpad.webapp import canonical_url | ||
149 | 12 | from canonical.testing.layers import LaunchpadFunctionalLayer | 11 | from canonical.testing.layers import LaunchpadFunctionalLayer |
150 | 13 | 12 | ||
151 | 14 | from lp.bugs.browser.bugsubscription import ( | 13 | from lp.bugs.browser.bugsubscription import ( |
152 | @@ -409,6 +408,44 @@ | |||
153 | 409 | self.bug = self.factory.makeBug() | 408 | self.bug = self.factory.makeBug() |
154 | 410 | self.person = self.factory.makePerson() | 409 | self.person = self.factory.makePerson() |
155 | 411 | 410 | ||
156 | 411 | def test_is_muted_false(self): | ||
157 | 412 | # BugMuteSelfView initialization sets the is_muted property. | ||
158 | 413 | # When the person has not muted the bug, it's false. | ||
159 | 414 | with person_logged_in(self.person): | ||
160 | 415 | self.assertFalse(self.bug.isMuted(self.person)) | ||
161 | 416 | view = create_initialized_view( | ||
162 | 417 | self.bug.default_bugtask, name="+mute") | ||
163 | 418 | self.assertFalse(view.is_muted) | ||
164 | 419 | |||
165 | 420 | def test_is_muted_true(self): | ||
166 | 421 | # BugMuteSelfView initialization sets the is_muted property. | ||
167 | 422 | # When the person has muted the bug, it's true. | ||
168 | 423 | with person_logged_in(self.person): | ||
169 | 424 | self.bug.mute(self.person, self.person) | ||
170 | 425 | self.assertTrue(self.bug.isMuted(self.person)) | ||
171 | 426 | view = create_initialized_view( | ||
172 | 427 | self.bug.default_bugtask, name="+mute") | ||
173 | 428 | self.assertTrue(view.is_muted) | ||
174 | 429 | |||
175 | 430 | def test_label_nonmuted(self): | ||
176 | 431 | # Label to use for the button. | ||
177 | 432 | with person_logged_in(self.person): | ||
178 | 433 | self.assertFalse(self.bug.isMuted(self.person)) | ||
179 | 434 | expected_label = "Mute bug mail for bug %s" % self.bug.id | ||
180 | 435 | view = create_initialized_view( | ||
181 | 436 | self.bug.default_bugtask, name="+mute") | ||
182 | 437 | self.assertEqual(expected_label, view.label) | ||
183 | 438 | |||
184 | 439 | def test_label_muted(self): | ||
185 | 440 | # Label to use for the button. | ||
186 | 441 | with person_logged_in(self.person): | ||
187 | 442 | self.bug.mute(self.person, self.person) | ||
188 | 443 | self.assertTrue(self.bug.isMuted(self.person)) | ||
189 | 444 | expected_label = "Unmute bug mail for bug %s" % self.bug.id | ||
190 | 445 | view = create_initialized_view( | ||
191 | 446 | self.bug.default_bugtask, name="+mute") | ||
192 | 447 | self.assertEqual(expected_label, view.label) | ||
193 | 448 | |||
194 | 412 | def test_bug_mute_self_view_mutes_bug(self): | 449 | def test_bug_mute_self_view_mutes_bug(self): |
195 | 413 | # The BugMuteSelfView mutes bug mail for the current user when | 450 | # The BugMuteSelfView mutes bug mail for the current user when |
196 | 414 | # its form is submitted. | 451 | # its form is submitted. |
197 | @@ -419,17 +456,13 @@ | |||
198 | 419 | form={'field.actions.mute': 'Mute bug mail'}) | 456 | form={'field.actions.mute': 'Mute bug mail'}) |
199 | 420 | self.assertTrue(self.bug.isMuted(self.person)) | 457 | self.assertTrue(self.bug.isMuted(self.person)) |
200 | 421 | 458 | ||
205 | 422 | def test_bug_mute_self_view_redirects_muted_users(self): | 459 | def test_bug_mute_self_view_unmutes_bug(self): |
206 | 423 | # The BugMuteSelfView redirects muted users to the +subscribe | 460 | # The BugMuteSelfView unmutes bug mail for the current user when |
207 | 424 | # page, where they can remove their muted subscription or change | 461 | # its form is submitted and the bug was already muted. |
204 | 425 | # their BugNotificationLevel. | ||
208 | 426 | with person_logged_in(self.person): | 462 | with person_logged_in(self.person): |
209 | 427 | self.bug.mute(self.person, self.person) | 463 | self.bug.mute(self.person, self.person) |
218 | 428 | mute_view = create_initialized_view( | 464 | self.assertTrue(self.bug.isMuted(self.person)) |
219 | 429 | self.bug.default_bugtask, name="+mute") | 465 | create_initialized_view( |
220 | 430 | response = mute_view.request.response | 466 | self.bug.default_bugtask, name="+mute", |
221 | 431 | self.assertEqual(302, response.getStatus()) | 467 | form={'field.actions.unmute': 'Unmute bug mail'}) |
222 | 432 | self.assertEqual( | 468 | self.assertFalse(self.bug.isMuted(self.person)) |
215 | 433 | canonical_url(self.bug.default_bugtask, | ||
216 | 434 | view_name="+subscribe"), | ||
217 | 435 | response.getHeader('Location')) | ||
223 | 436 | 469 | ||
224 | === modified file 'lib/lp/bugs/interfaces/bug.py' | |||
225 | --- lib/lp/bugs/interfaces/bug.py 2011-05-18 08:17:35 +0000 | |||
226 | +++ lib/lp/bugs/interfaces/bug.py 2011-05-23 09:01:25 +0000 | |||
227 | @@ -509,7 +509,9 @@ | |||
228 | 509 | @export_write_operation() | 509 | @export_write_operation() |
229 | 510 | @operation_for_version('devel') | 510 | @operation_for_version('devel') |
230 | 511 | def unmute(person, unmuted_by): | 511 | def unmute(person, unmuted_by): |
232 | 512 | """Remove a muted subscription for `person`.""" | 512 | """Remove a muted subscription for `person`. |
233 | 513 | |||
234 | 514 | Returns previously muted direct subscription, if any.""" | ||
235 | 513 | 515 | ||
236 | 514 | def getDirectSubscriptions(): | 516 | def getDirectSubscriptions(): |
237 | 515 | """A sequence of IBugSubscriptions directly linked to this bug.""" | 517 | """A sequence of IBugSubscriptions directly linked to this bug.""" |
238 | @@ -779,9 +781,9 @@ | |||
239 | 779 | schema=Interface, title=_('Target'), required=False), | 781 | schema=Interface, title=_('Target'), required=False), |
240 | 780 | nominations=List( | 782 | nominations=List( |
241 | 781 | title=_("Nominations to search through."), | 783 | title=_("Nominations to search through."), |
243 | 782 | value_type=Reference(schema=Interface), # IBugNomination | 784 | value_type=Reference(schema=Interface), # IBugNomination |
244 | 783 | required=False)) | 785 | required=False)) |
246 | 784 | @operation_returns_collection_of(Interface) # IBugNomination | 786 | @operation_returns_collection_of(Interface) # IBugNomination |
247 | 785 | @export_read_operation() | 787 | @export_read_operation() |
248 | 786 | def getNominations(target=None, nominations=None): | 788 | def getNominations(target=None, nominations=None): |
249 | 787 | """Return a list of all IBugNominations for this bug. | 789 | """Return a list of all IBugNominations for this bug. |
250 | 788 | 790 | ||
251 | === modified file 'lib/lp/bugs/model/bug.py' | |||
252 | --- lib/lp/bugs/model/bug.py 2011-05-18 08:17:35 +0000 | |||
253 | +++ lib/lp/bugs/model/bug.py 2011-05-23 09:01:25 +0000 | |||
254 | @@ -54,6 +54,7 @@ | |||
255 | 54 | Count, | 54 | Count, |
256 | 55 | Desc, | 55 | Desc, |
257 | 56 | Exists, | 56 | Exists, |
258 | 57 | In, | ||
259 | 57 | Join, | 58 | Join, |
260 | 58 | LeftJoin, | 59 | LeftJoin, |
261 | 59 | Max, | 60 | Max, |
262 | @@ -531,7 +532,7 @@ | |||
263 | 531 | parent = message_by_id.get(parent.id, parent) | 532 | parent = message_by_id.get(parent.id, parent) |
264 | 532 | else: | 533 | else: |
265 | 533 | message, bugmessage = row | 534 | message, bugmessage = row |
267 | 534 | parent = None # parent attribute is not going to be accessed. | 535 | parent = None # parent attribute is not going to be accessed. |
268 | 535 | index = bugmessage.index | 536 | index = bugmessage.index |
269 | 536 | result = IndexedMessage(message, inside, index, parent) | 537 | result = IndexedMessage(message, inside, index, parent) |
270 | 537 | if include_parents: | 538 | if include_parents: |
271 | @@ -891,6 +892,7 @@ | |||
272 | 891 | person = unmuted_by | 892 | person = unmuted_by |
273 | 892 | mutes = self._getMutes(person) | 893 | mutes = self._getMutes(person) |
274 | 893 | store.remove(mutes.one()) | 894 | store.remove(mutes.one()) |
275 | 895 | return self.getSubscriptionForPerson(person) | ||
276 | 894 | 896 | ||
277 | 895 | @property | 897 | @property |
278 | 896 | def subscriptions(self): | 898 | def subscriptions(self): |
279 | @@ -2249,7 +2251,9 @@ | |||
280 | 2249 | return IStore(BugSubscription).find( | 2251 | return IStore(BugSubscription).find( |
281 | 2250 | BugSubscription, | 2252 | BugSubscription, |
282 | 2251 | BugSubscription.bug_notification_level >= self.level, | 2253 | BugSubscription.bug_notification_level >= self.level, |
284 | 2252 | BugSubscription.bug == self.bug) | 2254 | BugSubscription.bug == self.bug, |
285 | 2255 | Not(In(BugSubscription.person_id, | ||
286 | 2256 | Select(BugMute.person_id, BugMute.bug_id==self.bug.id)))) | ||
287 | 2253 | 2257 | ||
288 | 2254 | @cachedproperty | 2258 | @cachedproperty |
289 | 2255 | @freeze(BugSubscriptionSet) | 2259 | @freeze(BugSubscriptionSet) |
290 | @@ -2262,12 +2266,14 @@ | |||
291 | 2262 | BugSubscription, | 2266 | BugSubscription, |
292 | 2263 | BugSubscription.bug_notification_level >= self.level, | 2267 | BugSubscription.bug_notification_level >= self.level, |
293 | 2264 | BugSubscription.bug_id == Bug.id, | 2268 | BugSubscription.bug_id == Bug.id, |
295 | 2265 | Bug.duplicateof == self.bug) | 2269 | Bug.duplicateof == self.bug, |
296 | 2270 | Not(In(BugSubscription.person_id, | ||
297 | 2271 | Select(BugMute.person_id, BugMute.bug_id==Bug.id)))) | ||
298 | 2266 | 2272 | ||
299 | 2267 | @cachedproperty | 2273 | @cachedproperty |
300 | 2268 | @freeze(BugSubscriptionSet) | 2274 | @freeze(BugSubscriptionSet) |
301 | 2269 | def duplicate_only_subscriptions(self): | 2275 | def duplicate_only_subscriptions(self): |
303 | 2270 | """Subscripitions to duplicates of the bug. | 2276 | """Subscriptions to duplicates of the bug. |
304 | 2271 | 2277 | ||
305 | 2272 | Excludes subscriptions for people who have a direct subscription or | 2278 | Excludes subscriptions for people who have a direct subscription or |
306 | 2273 | are also notified for another reason. | 2279 | are also notified for another reason. |
307 | @@ -2308,11 +2314,15 @@ | |||
308 | 2308 | if self.bug.private: | 2314 | if self.bug.private: |
309 | 2309 | return BugSubscriberSet() | 2315 | return BugSubscriberSet() |
310 | 2310 | else: | 2316 | else: |
311 | 2317 | muted = IStore(BugMute).find( | ||
312 | 2318 | Person, | ||
313 | 2319 | BugMute.person_id==Person.id, | ||
314 | 2320 | BugMute.bug==self.bug) | ||
315 | 2311 | return BugSubscriberSet().union( | 2321 | return BugSubscriberSet().union( |
316 | 2312 | self.structural_subscriptions.subscribers, | 2322 | self.structural_subscriptions.subscribers, |
317 | 2313 | self.all_pillar_owners_without_bug_supervisors, | 2323 | self.all_pillar_owners_without_bug_supervisors, |
318 | 2314 | self.all_assignees).difference( | 2324 | self.all_assignees).difference( |
320 | 2315 | self.direct_subscriptions.subscribers) | 2325 | self.direct_subscriptions.subscribers).difference(muted) |
321 | 2316 | 2326 | ||
322 | 2317 | @cachedproperty | 2327 | @cachedproperty |
323 | 2318 | def indirect_subscribers(self): | 2328 | def indirect_subscribers(self): |
324 | 2319 | 2329 | ||
325 | === modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py' | |||
326 | --- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2011-05-16 16:57:55 +0000 | |||
327 | +++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2011-05-23 09:01:25 +0000 | |||
328 | @@ -132,21 +132,42 @@ | |||
329 | 132 | return BugSubscriptionInfo( | 132 | return BugSubscriptionInfo( |
330 | 133 | self.bug, BugNotificationLevel.LIFECYCLE) | 133 | self.bug, BugNotificationLevel.LIFECYCLE) |
331 | 134 | 134 | ||
332 | 135 | def _create_direct_subscriptions(self): | ||
333 | 136 | subscribers = ( | ||
334 | 137 | self.factory.makePerson(), | ||
335 | 138 | self.factory.makePerson()) | ||
336 | 139 | with person_logged_in(self.bug.owner): | ||
337 | 140 | subscriptions = tuple( | ||
338 | 141 | self.bug.subscribe(subscriber, subscriber) | ||
339 | 142 | for subscriber in subscribers) | ||
340 | 143 | return subscribers, subscriptions | ||
341 | 144 | |||
342 | 135 | def test_direct(self): | 145 | def test_direct(self): |
343 | 136 | # The set of direct subscribers. | 146 | # The set of direct subscribers. |
351 | 137 | subscribers = ( | 147 | subscribers, subscriptions = self._create_direct_subscriptions() |
345 | 138 | self.factory.makePerson(), | ||
346 | 139 | self.factory.makePerson()) | ||
347 | 140 | with person_logged_in(self.bug.owner): | ||
348 | 141 | subscriptions = tuple( | ||
349 | 142 | self.bug.subscribe(subscriber, subscriber) | ||
350 | 143 | for subscriber in subscribers) | ||
352 | 144 | found_subscriptions = self.getInfo().direct_subscriptions | 148 | found_subscriptions = self.getInfo().direct_subscriptions |
353 | 145 | self.assertEqual(set(subscriptions), found_subscriptions) | 149 | self.assertEqual(set(subscriptions), found_subscriptions) |
354 | 146 | self.assertEqual(subscriptions, found_subscriptions.sorted) | 150 | self.assertEqual(subscriptions, found_subscriptions.sorted) |
355 | 147 | self.assertEqual(set(subscribers), found_subscriptions.subscribers) | 151 | self.assertEqual(set(subscribers), found_subscriptions.subscribers) |
356 | 148 | self.assertEqual(subscribers, found_subscriptions.subscribers.sorted) | 152 | self.assertEqual(subscribers, found_subscriptions.subscribers.sorted) |
357 | 149 | 153 | ||
358 | 154 | def test_muted_direct(self): | ||
359 | 155 | # If a direct is muted, it is not listed. | ||
360 | 156 | subscribers, subscriptions = self._create_direct_subscriptions() | ||
361 | 157 | with person_logged_in(subscribers[0]): | ||
362 | 158 | self.bug.mute(subscribers[0], subscribers[0]) | ||
363 | 159 | found_subscriptions = self.getInfo().direct_subscriptions | ||
364 | 160 | self.assertEqual(set([subscriptions[1]]), found_subscriptions) | ||
365 | 161 | |||
366 | 162 | def _create_duplicate_subscription(self): | ||
367 | 163 | duplicate_bug = self.factory.makeBug(product=self.target) | ||
368 | 164 | with person_logged_in(duplicate_bug.owner): | ||
369 | 165 | duplicate_bug.markAsDuplicate(self.bug) | ||
370 | 166 | duplicate_bug_subscription = ( | ||
371 | 167 | duplicate_bug.getSubscriptionForPerson( | ||
372 | 168 | duplicate_bug.owner)) | ||
373 | 169 | return duplicate_bug, duplicate_bug_subscription | ||
374 | 170 | |||
375 | 150 | def test_duplicate(self): | 171 | def test_duplicate(self): |
376 | 151 | # The set of subscribers from duplicate bugs. | 172 | # The set of subscribers from duplicate bugs. |
377 | 152 | found_subscriptions = self.getInfo().duplicate_subscriptions | 173 | found_subscriptions = self.getInfo().duplicate_subscriptions |
378 | @@ -154,12 +175,8 @@ | |||
379 | 154 | self.assertEqual((), found_subscriptions.sorted) | 175 | self.assertEqual((), found_subscriptions.sorted) |
380 | 155 | self.assertEqual(set(), found_subscriptions.subscribers) | 176 | self.assertEqual(set(), found_subscriptions.subscribers) |
381 | 156 | self.assertEqual((), found_subscriptions.subscribers.sorted) | 177 | self.assertEqual((), found_subscriptions.subscribers.sorted) |
388 | 157 | duplicate_bug = self.factory.makeBug(product=self.target) | 178 | duplicate_bug, duplicate_bug_subscription = ( |
389 | 158 | with person_logged_in(duplicate_bug.owner): | 179 | self._create_duplicate_subscription()) |
384 | 159 | duplicate_bug.markAsDuplicate(self.bug) | ||
385 | 160 | duplicate_bug_subscription = ( | ||
386 | 161 | duplicate_bug.getSubscriptionForPerson( | ||
387 | 162 | duplicate_bug.owner)) | ||
390 | 163 | found_subscriptions = self.getInfo().duplicate_subscriptions | 180 | found_subscriptions = self.getInfo().duplicate_subscriptions |
391 | 164 | self.assertEqual( | 181 | self.assertEqual( |
392 | 165 | set([duplicate_bug_subscription]), | 182 | set([duplicate_bug_subscription]), |
393 | @@ -174,6 +191,15 @@ | |||
394 | 174 | (duplicate_bug.owner,), | 191 | (duplicate_bug.owner,), |
395 | 175 | found_subscriptions.subscribers.sorted) | 192 | found_subscriptions.subscribers.sorted) |
396 | 176 | 193 | ||
397 | 194 | def test_muted_duplicate(self): | ||
398 | 195 | # If a duplicate is muted, it is not listed. | ||
399 | 196 | duplicate_bug, duplicate_bug_subscription = ( | ||
400 | 197 | self._create_duplicate_subscription()) | ||
401 | 198 | with person_logged_in(duplicate_bug.owner): | ||
402 | 199 | self.bug.mute(duplicate_bug.owner, duplicate_bug.owner) | ||
403 | 200 | found_subscriptions = self.getInfo().duplicate_subscriptions | ||
404 | 201 | self.assertEqual(set(), found_subscriptions) | ||
405 | 202 | |||
406 | 177 | def test_duplicate_for_private_bug(self): | 203 | def test_duplicate_for_private_bug(self): |
407 | 178 | # The set of subscribers from duplicate bugs is always empty when the | 204 | # The set of subscribers from duplicate bugs is always empty when the |
408 | 179 | # master bug is private. | 205 | # master bug is private. |
409 | @@ -274,11 +300,7 @@ | |||
410 | 274 | (bugtask.pillar.owner, bugtask2.pillar.owner), | 300 | (bugtask.pillar.owner, bugtask2.pillar.owner), |
411 | 275 | found_owners.sorted) | 301 | found_owners.sorted) |
412 | 276 | 302 | ||
418 | 277 | def test_also_notified_subscribers(self): | 303 | def _create_also_notified_subscribers(self): |
414 | 278 | # The set of also notified subscribers. | ||
415 | 279 | found_subscribers = self.getInfo().also_notified_subscribers | ||
416 | 280 | self.assertEqual(set(), found_subscribers) | ||
417 | 281 | self.assertEqual((), found_subscribers.sorted) | ||
419 | 282 | # Add an assignee, a bug supervisor and a structural subscriber. | 304 | # Add an assignee, a bug supervisor and a structural subscriber. |
420 | 283 | bugtask = self.bug.default_bugtask | 305 | bugtask = self.bug.default_bugtask |
421 | 284 | assignee = self.factory.makePerson() | 306 | assignee = self.factory.makePerson() |
422 | @@ -291,6 +313,15 @@ | |||
423 | 291 | with person_logged_in(structural_subscriber): | 313 | with person_logged_in(structural_subscriber): |
424 | 292 | bugtask.target.addSubscription( | 314 | bugtask.target.addSubscription( |
425 | 293 | structural_subscriber, structural_subscriber) | 315 | structural_subscriber, structural_subscriber) |
426 | 316 | return assignee, supervisor, structural_subscriber | ||
427 | 317 | |||
428 | 318 | def test_also_notified_subscribers(self): | ||
429 | 319 | # The set of also notified subscribers. | ||
430 | 320 | found_subscribers = self.getInfo().also_notified_subscribers | ||
431 | 321 | self.assertEqual(set(), found_subscribers) | ||
432 | 322 | self.assertEqual((), found_subscribers.sorted) | ||
433 | 323 | assignee, supervisor, structural_subscriber = ( | ||
434 | 324 | self._create_also_notified_subscribers()) | ||
435 | 294 | # Add a direct subscription. | 325 | # Add a direct subscription. |
436 | 295 | direct_subscriber = self.factory.makePerson() | 326 | direct_subscriber = self.factory.makePerson() |
437 | 296 | with person_logged_in(self.bug.owner): | 327 | with person_logged_in(self.bug.owner): |
438 | @@ -305,6 +336,30 @@ | |||
439 | 305 | (assignee, supervisor, structural_subscriber), | 336 | (assignee, supervisor, structural_subscriber), |
440 | 306 | found_subscribers.sorted) | 337 | found_subscribers.sorted) |
441 | 307 | 338 | ||
442 | 339 | def test_muted_also_notified_subscribers(self): | ||
443 | 340 | # If someone is muted, they are not listed in the | ||
444 | 341 | # also_notified_subscribers. | ||
445 | 342 | assignee, supervisor, structural_subscriber = ( | ||
446 | 343 | self._create_also_notified_subscribers()) | ||
447 | 344 | # As a control, we first show that the | ||
448 | 345 | # the assignee, supervisor and structural subscriber do show up | ||
449 | 346 | # when they are not muted. | ||
450 | 347 | found_subscribers = self.getInfo().also_notified_subscribers | ||
451 | 348 | self.assertEqual( | ||
452 | 349 | set([assignee, supervisor, structural_subscriber]), | ||
453 | 350 | found_subscribers) | ||
454 | 351 | # Now we mute all of the subscribers. | ||
455 | 352 | with person_logged_in(assignee): | ||
456 | 353 | self.bug.mute(assignee, assignee) | ||
457 | 354 | with person_logged_in(supervisor): | ||
458 | 355 | self.bug.mute(supervisor, supervisor) | ||
459 | 356 | with person_logged_in(structural_subscriber): | ||
460 | 357 | self.bug.mute(structural_subscriber, structural_subscriber) | ||
461 | 358 | # Now we don't see them. | ||
462 | 359 | found_subscribers = self.getInfo().also_notified_subscribers | ||
463 | 360 | self.assertEqual( | ||
464 | 361 | set(), found_subscribers) | ||
465 | 362 | |||
466 | 308 | def test_also_notified_subscribers_for_private_bug(self): | 363 | def test_also_notified_subscribers_for_private_bug(self): |
467 | 309 | # The set of also notified subscribers is always empty when the master | 364 | # The set of also notified subscribers is always empty when the master |
468 | 310 | # bug is private. | 365 | # bug is private. |
469 | @@ -472,7 +527,7 @@ | |||
470 | 472 | self.info.all_pillar_owners_without_bug_supervisors | 527 | self.info.all_pillar_owners_without_bug_supervisors |
471 | 473 | 528 | ||
472 | 474 | def test_also_notified_subscribers(self): | 529 | def test_also_notified_subscribers(self): |
474 | 475 | with self.exactly_x_queries(5): | 530 | with self.exactly_x_queries(6): |
475 | 476 | self.info.also_notified_subscribers | 531 | self.info.also_notified_subscribers |
476 | 477 | 532 | ||
477 | 478 | def test_also_notified_subscribers_later(self): | 533 | def test_also_notified_subscribers_later(self): |
478 | @@ -482,11 +537,11 @@ | |||
479 | 482 | self.info.all_pillar_owners_without_bug_supervisors | 537 | self.info.all_pillar_owners_without_bug_supervisors |
480 | 483 | self.info.direct_subscriptions.subscribers | 538 | self.info.direct_subscriptions.subscribers |
481 | 484 | self.info.structural_subscriptions.subscribers | 539 | self.info.structural_subscriptions.subscribers |
483 | 485 | with self.exactly_x_queries(0): | 540 | with self.exactly_x_queries(1): |
484 | 486 | self.info.also_notified_subscribers | 541 | self.info.also_notified_subscribers |
485 | 487 | 542 | ||
486 | 488 | def test_indirect_subscribers(self): | 543 | def test_indirect_subscribers(self): |
488 | 489 | with self.exactly_x_queries(6): | 544 | with self.exactly_x_queries(7): |
489 | 490 | self.info.indirect_subscribers | 545 | self.info.indirect_subscribers |
490 | 491 | 546 | ||
491 | 492 | def test_indirect_subscribers_later(self): | 547 | def test_indirect_subscribers_later(self): |
492 | 493 | 548 | ||
493 | === modified file 'lib/lp/bugs/tests/test_bug.py' | |||
494 | --- lib/lp/bugs/tests/test_bug.py 2011-05-18 08:17:35 +0000 | |||
495 | +++ lib/lp/bugs/tests/test_bug.py 2011-05-23 09:01:25 +0000 | |||
496 | @@ -105,6 +105,22 @@ | |||
497 | 105 | self.bug.unmute(self.person, self.person) | 105 | self.bug.unmute(self.person, self.person) |
498 | 106 | self.assertFalse(self.bug.isMuted(self.person)) | 106 | self.assertFalse(self.bug.isMuted(self.person)) |
499 | 107 | 107 | ||
500 | 108 | def test_unmute_returns_direct_subscription(self): | ||
501 | 109 | # Bug.unmute() returns the previously muted direct subscription, if | ||
502 | 110 | # any. | ||
503 | 111 | with person_logged_in(self.person): | ||
504 | 112 | self.bug.mute(self.person, self.person) | ||
505 | 113 | self.assertEqual(True, self.bug.isMuted(self.person)) | ||
506 | 114 | self.assertEqual(None, self.bug.unmute(self.person, self.person)) | ||
507 | 115 | self.assertEqual(False, self.bug.isMuted(self.person)) | ||
508 | 116 | subscription = self.bug.subscribe( | ||
509 | 117 | self.person, self.person, | ||
510 | 118 | level=BugNotificationLevel.METADATA) | ||
511 | 119 | self.bug.mute(self.person, self.person) | ||
512 | 120 | self.assertEqual(True, self.bug.isMuted(self.person)) | ||
513 | 121 | self.assertEqual( | ||
514 | 122 | subscription, self.bug.unmute(self.person, self.person)) | ||
515 | 123 | |||
516 | 108 | def test_unmute_mutes_unmuter(self): | 124 | def test_unmute_mutes_unmuter(self): |
517 | 109 | # When exposed in the web API, the unmute method regards the | 125 | # When exposed in the web API, the unmute method regards the |
518 | 110 | # first, `person` argument as optional, and the second | 126 | # first, `person` argument as optional, and the second |