Merge lp:~allenap/launchpad/subscribe-to-tag-bug-151129-6 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 12307
Proposed branch: lp:~allenap/launchpad/subscribe-to-tag-bug-151129-6
Merge into: lp:launchpad
Diff against target: 500 lines (+181/-74)
7 files modified
lib/lp/bugs/browser/bug.py (+11/-23)
lib/lp/bugs/configure.zcml (+26/-0)
lib/lp/bugs/doc/bugsubscription.txt (+5/-5)
lib/lp/bugs/interfaces/bug.py (+6/-0)
lib/lp/bugs/model/bug.py (+61/-34)
lib/lp/bugs/model/tests/test_bug.py (+20/-12)
lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+52/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/subscribe-to-tag-bug-151129-6
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+48295@code.launchpad.net

Commit message

[r=danilo][ui=none][bug=681326] Add a duplicate_only_subscriptions set to BugSubscriptionInfo.

Description of the change

Some more work on infiltrating BugSubscriptionInfo into the position
of knowing all about subscriptions, and some other bits:

lib/lp/bugs/browser/bug.py

  Use the new Bug.getSubscriptionInfo() method in BugViewMixin.

lib/lp/bugs/configure.zcml

  Security declaration for BugSubscriptionInfo and
  Bug.getSubscriptionInfo().

lib/lp/bugs/doc/bugsubscription.txt

  BugSubscriptionInfo is immutable, and returns immutable data types,
  so there are tuples here where there were once lists.

lib/lp/bugs/interfaces/bug.py

  Interface declaration for the new getSubscriptionInfo() method.

lib/lp/bugs/model/bug.py

  Implementation of getSubscriptionInfo(). I've also used it in
  getDirectSubscriptions(), getDirectSubscribers() and
  getSubscribersFromDuplicates().

  BugSubscriptionInfo now implements IHasBug. This is true, and it
  also means that a security adapter can be selected for it. See the
  changes in test_bugsubscriptioninfo.py.

  I've put a lot of the design goals I had in mind for
  BugSubscriptionInfo into its docstring. Trying to convince people
  that it's a good idea to use :)

  Implementation of the duplicate_only_subscriptions set.

lib/lp/bugs/model/tests/test_bug.py

  Test for getSubscriptionInfo().

lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py

  Test for the new duplicate_only_subscriptions set.

  Test for BugSubscriptionInfo permissions.

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :
Download full text (4.7 KiB)

Hi Gavin,

This looks like a very nice refactoring branch. A few questions inline.

> === modified file 'lib/lp/bugs/browser/bug.py'
> --- lib/lp/bugs/browser/bug.py 2011-01-18 20:49:35 +0000
> +++ lib/lp/bugs/browser/bug.py 2011-02-02 11:14:58 +0000
> @@ -414,39 +414,28 @@
> """Mix-in class to share methods between bug and portlet views."""
>
> @cachedproperty
> + def subscription_info(self):
> + return IBug(self.context).getSubscriptionInfo()
> +
> + @property
> def direct_subscribers(self):

Is the change here from @cachedproperty to @property for
direct_subscribers intentional?

> """Return the list of direct subscribers."""
> - if IBug.providedBy(self.context):
> - return set(self.context.getDirectSubscribers())
> - elif IBugTask.providedBy(self.context):
> - return set(self.context.bug.getDirectSubscribers())
> - else:
> - raise NotImplementedError(
> - 'direct_subscribers is not provided by %s' % self)
> + return self.subscription_info.direct_subscriptions.subscribers

IOW, it still includes a few indirections, and
direct_subscriptions.subscribers might be expensive to evaluate.
Perhaps the code below will answer my question :)

>
> - @cachedproperty
> + @property
> def duplicate_subscribers(self):

This tells me that these are indeed intentional.

> === modified file 'lib/lp/bugs/model/bug.py'
...
> @@ -851,8 +856,8 @@
> """
> # "Also notified" and duplicate subscribers are mutually
> # exclusive, so return both lists.
> - indirect_subscribers = (
> - self.getAlsoNotifiedSubscribers(recipients, level) +
> + indirect_subscribers = chain(
> + self.getAlsoNotifiedSubscribers(recipients, level),
> self.getSubscribersFromDuplicates(recipients, level))

I didn't know about itertools.chain, nice :)

> @@ -887,37 +892,16 @@
> See the comment in getDirectSubscribers for a description of the
> recipients argument.
> """
> - if self.private:
> - return []
> -
> - if level is None:
> - notification_level = BugNotificationLevel.NOTHING
> - else:
> - notification_level = level
> -
> - dupe_details = dict(
> - Store.of(self).find(
> - (Person, Bug),
> - BugSubscription.person == Person.id,
> - BugSubscription.bug_id == Bug.id,
> - BugSubscription.bug_notification_level >= notification_level,
> - Bug.duplicateof == self.id))
> -
> - dupe_subscribers = set(dupe_details)
> -
> - # Direct and "also notified" subscribers take precedence over
> - # subscribers from dupes. Note that we don't supply recipients
> - # here because we are doing this to /remove/ subscribers.
> - dupe_subscribers -= set(self.getDirectSubscribers())
> - dupe_subscribers -= set(self.getAlsoNotifiedSubscribers())
> + info = self.getSubscriptionInfo()
>
> if recipients is not None:
> - for subscriber in dupe_subscribers:
> + # ...

Read more...

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (5.7 KiB)

> Hi Gavin,
>
> This looks like a very nice refactoring branch. A few questions inline.
>
> > === modified file 'lib/lp/bugs/browser/bug.py'
> > --- lib/lp/bugs/browser/bug.py 2011-01-18 20:49:35 +0000
> > +++ lib/lp/bugs/browser/bug.py 2011-02-02 11:14:58 +0000
> > @@ -414,39 +414,28 @@
> > """Mix-in class to share methods between bug and portlet views."""
> >
> > @cachedproperty
> > + def subscription_info(self):
> > + return IBug(self.context).getSubscriptionInfo()
> > +
> > + @property
> > def direct_subscribers(self):
>
> Is the change here from @cachedproperty to @property for
> direct_subscribers intentional?

The properties of BugSubscriptionInfo are all cached, so there's no
need to cache here.

>
> > """Return the list of direct subscribers."""
> > - if IBug.providedBy(self.context):
> > - return set(self.context.getDirectSubscribers())
> > - elif IBugTask.providedBy(self.context):
> > - return set(self.context.bug.getDirectSubscribers())
> > - else:
> > - raise NotImplementedError(
> > - 'direct_subscribers is not provided by %s' % self)
> > + return self.subscription_info.direct_subscriptions.subscribers
>
> IOW, it still includes a few indirections, and
> direct_subscriptions.subscribers might be expensive to evaluate.
> Perhaps the code below will answer my question :)

It should be okay to evaluate because of the design of
BugSubscriptionInfo. I doubt that it'll be more expensive.

>
> >
> > - @cachedproperty
> > + @property
> > def duplicate_subscribers(self):
>
> This tells me that these are indeed intentional.

Yes, same as before.

>
> > === modified file 'lib/lp/bugs/model/bug.py'
> ...
> > @@ -851,8 +856,8 @@
> > """
> > # "Also notified" and duplicate subscribers are mutually
> > # exclusive, so return both lists.
> > - indirect_subscribers = (
> > - self.getAlsoNotifiedSubscribers(recipients, level) +
> > + indirect_subscribers = chain(
> > + self.getAlsoNotifiedSubscribers(recipients, level),
> > self.getSubscribersFromDuplicates(recipients, level))
>
> I didn't know about itertools.chain, nice :)

itertools is a great module, though I use chain() and groupby() far
more than the other functions in there.

>
> > @@ -887,37 +892,16 @@
> > See the comment in getDirectSubscribers for a description of the
> > recipients argument.
> > """
> > - if self.private:
> > - return []
> > -
> > - if level is None:
> > - notification_level = BugNotificationLevel.NOTHING
> > - else:
> > - notification_level = level
> > -
> > - dupe_details = dict(
> > - Store.of(self).find(
> > - (Person, Bug),
> > - BugSubscription.person == Person.id,
> > - BugSubscription.bug_id == Bug.id,
> > - BugSubscription.bug_notification_level >=
> notification_level,
> > - Bug.duplicateof == self.id))
> > -
> > - dupe_subscribers = set(dupe_details)
> > -
> > - # ...

Read more...

Revision history for this message
Данило Шеган (danilo) wrote :

Thanks, all my concerns have been addressed.

  review approve
  merge approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bug.py'
2--- lib/lp/bugs/browser/bug.py 2011-01-18 20:49:35 +0000
3+++ lib/lp/bugs/browser/bug.py 2011-02-02 13:33:41 +0000
4@@ -98,7 +98,6 @@
5 from lp.bugs.interfaces.bugtask import (
6 BugTaskSearchParams,
7 BugTaskStatus,
8- IBugTask,
9 IFrontPageBugTaskSearch,
10 )
11 from lp.bugs.interfaces.bugwatch import IBugWatchSet
12@@ -414,39 +413,28 @@
13 """Mix-in class to share methods between bug and portlet views."""
14
15 @cachedproperty
16+ def subscription_info(self):
17+ return IBug(self.context).getSubscriptionInfo()
18+
19+ @property
20 def direct_subscribers(self):
21 """Return the list of direct subscribers."""
22- if IBug.providedBy(self.context):
23- return set(self.context.getDirectSubscribers())
24- elif IBugTask.providedBy(self.context):
25- return set(self.context.bug.getDirectSubscribers())
26- else:
27- raise NotImplementedError(
28- 'direct_subscribers is not provided by %s' % self)
29+ return self.subscription_info.direct_subscriptions.subscribers
30
31- @cachedproperty
32+ @property
33 def duplicate_subscribers(self):
34 """Return the list of subscribers from duplicates.
35
36- Don't use getSubscribersFromDuplicates here because that method
37- omits a user if the user is also a direct or indirect subscriber.
38- getSubscriptionsFromDuplicates doesn't, so find person objects via
39- this method.
40+ This includes all subscribers who are also direct or indirect
41+ subscribers.
42 """
43- if IBug.providedBy(self.context):
44- dupe_subs = self.context.getSubscriptionsFromDuplicates()
45- return set(sub.person for sub in dupe_subs)
46- elif IBugTask.providedBy(self.context):
47- dupe_subs = self.context.bug.getSubscriptionsFromDuplicates()
48- return set(sub.person for sub in dupe_subs)
49- else:
50- raise NotImplementedError(
51- 'duplicate_subscribers is not implemented for %s' % self)
52+ return self.subscription_info.duplicate_subscriptions.subscribers
53
54 @cachedproperty
55 def subscriber_ids(self):
56 """Return a dictionary mapping a css_name to user name."""
57- subscribers = self.direct_subscribers.union(
58+ subscribers = set().union(
59+ self.direct_subscribers,
60 self.duplicate_subscribers)
61
62 # The current user has to be in subscribers_id so
63
64=== modified file 'lib/lp/bugs/configure.zcml'
65--- lib/lp/bugs/configure.zcml 2011-01-31 14:07:47 +0000
66+++ lib/lp/bugs/configure.zcml 2011-02-02 13:33:41 +0000
67@@ -638,6 +638,31 @@
68 set_schema=".interfaces.bugsubscriptionfilter.IBugSubscriptionFilterAttributes" />
69 </class>
70
71+ <!-- BugSubscriptionInfo -->
72+
73+ <class class=".model.bug.BugSubscriptionInfo">
74+ <!-- Instance variables -->
75+ <require
76+ permission="launchpad.View"
77+ attributes="
78+ bug
79+ level
80+ " />
81+ <!-- Properties -->
82+ <require
83+ permission="launchpad.View"
84+ attributes="
85+ all_assignees
86+ all_pillar_owners_without_bug_supervisors
87+ also_notified_subscribers
88+ direct_subscriptions
89+ duplicate_only_subscriptions
90+ duplicate_subscriptions
91+ indirect_subscribers
92+ structural_subscriptions
93+ " />
94+ </class>
95+
96 <facet
97 facet="bugs">
98
99@@ -719,6 +744,7 @@
100 getSubscriptionsFromDuplicates
101 getSubscribersForPerson
102 getSubscriptionForPerson
103+ getSubscriptionInfo
104 indexed_messages
105 getAlsoNotifiedSubscribers
106 getBugWatch
107
108=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
109--- lib/lp/bugs/doc/bugsubscription.txt 2011-01-27 09:47:06 +0000
110+++ lib/lp/bugs/doc/bugsubscription.txt 2011-02-02 13:33:41 +0000
111@@ -101,7 +101,7 @@
112 >>> linux_source_bug.getAlsoNotifiedSubscribers()
113 [<Person at ...>]
114 >>> linux_source_bug.getSubscribersFromDuplicates()
115- []
116+ ()
117
118 It is also possible to get the list of indirect subscribers for an
119 individual bug task.
120@@ -137,7 +137,7 @@
121 Ubuntu Team
122
123 >>> linux_source_bug.getSubscribersFromDuplicates()
124- []
125+ ()
126
127 >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
128 Sample Person
129@@ -161,7 +161,7 @@
130 Ubuntu Team
131
132 >>> linux_source_bug.getSubscribersFromDuplicates()
133- []
134+ ()
135 >>> print_displayname(linux_source_bug.getAlsoNotifiedSubscribers())
136 No Privileges Person
137 Sample Person
138@@ -365,7 +365,7 @@
139 >>> subscription_keybuk = linux_source.addBugSubscription(
140 ... keybuk, keybuk)
141 >>> linux_source_bug.getSubscribersFromDuplicates()
142- []
143+ ()
144
145 When a bug is marked private, all its indirect subscribers become direct
146 subscribers.
147@@ -409,7 +409,7 @@
148 []
149
150 >>> linux_source_bug.getSubscribersFromDuplicates()
151- []
152+ ()
153
154 Direct subscriptions always take precedence over indirect
155 subscriptions. So, if we unmark the above bug as private,
156
157=== modified file 'lib/lp/bugs/interfaces/bug.py'
158--- lib/lp/bugs/interfaces/bug.py 2011-01-31 14:07:47 +0000
159+++ lib/lp/bugs/interfaces/bug.py 2011-02-02 13:33:41 +0000
160@@ -529,6 +529,12 @@
161 If no such `BugSubscription` exists, return None.
162 """
163
164+ def getSubscriptionInfo(level):
165+ """Return a `BugSubscriptionInfo` at the given `level`.
166+
167+ :param level: A member of `BugNotificationLevel`.
168+ """
169+
170 def getBugNotificationRecipients(duplicateof=None, old_bug=None,
171 include_master_dupe_subscribers=False):
172 """Return a complete INotificationRecipientSet instance.
173
174=== modified file 'lib/lp/bugs/model/bug.py'
175--- lib/lp/bugs/model/bug.py 2011-01-31 09:24:13 +0000
176+++ lib/lp/bugs/model/bug.py 2011-02-02 13:33:41 +0000
177@@ -26,6 +26,7 @@
178 )
179 from email.Utils import make_msgid
180 from functools import wraps
181+from itertools import chain
182 import operator
183 import re
184
185@@ -90,6 +91,7 @@
186 )
187 from canonical.launchpad.helpers import shortlist
188 from canonical.launchpad.interfaces.launchpad import (
189+ IHasBug,
190 ILaunchpadCelebrities,
191 IPersonRoles,
192 )
193@@ -436,7 +438,7 @@
194 """See `IBug`."""
195 indexed_messages = self._indexed_messages(include_bugmessage=True)
196 for indexed_message, bugmessage in indexed_messages:
197- if bugmessage.index != indexed_message.index:
198+ if bugmessage.index != indexed_message.index:
199 bugmessage.index = indexed_message.index
200
201 @property
202@@ -822,10 +824,13 @@
203 BugSubscription.bug_id == self.id).order_by(BugSubscription.id)
204 return DecoratedResultSet(results, operator.itemgetter(1))
205
206+ def getSubscriptionInfo(self, level=BugNotificationLevel.NOTHING):
207+ """See `IBug`."""
208+ return BugSubscriptionInfo(self, level)
209+
210 def getDirectSubscriptions(self):
211 """See `IBug`."""
212- return BugSubscriptionInfo(
213- self, BugNotificationLevel.NOTHING).direct_subscriptions
214+ return self.getSubscriptionInfo().direct_subscriptions
215
216 def getDirectSubscribers(self, recipients=None, level=None):
217 """See `IBug`.
218@@ -837,7 +842,7 @@
219 """
220 if level is None:
221 level = BugNotificationLevel.NOTHING
222- subscriptions = BugSubscriptionInfo(self, level).direct_subscriptions
223+ subscriptions = self.getSubscriptionInfo(level).direct_subscriptions
224 if recipients is not None:
225 for subscriber in subscriptions.subscribers:
226 recipients.addDirectSubscriber(subscriber)
227@@ -851,8 +856,8 @@
228 """
229 # "Also notified" and duplicate subscribers are mutually
230 # exclusive, so return both lists.
231- indirect_subscribers = (
232- self.getAlsoNotifiedSubscribers(recipients, level) +
233+ indirect_subscribers = chain(
234+ self.getAlsoNotifiedSubscribers(recipients, level),
235 self.getSubscribersFromDuplicates(recipients, level))
236
237 # Remove security proxy for the sort key, but return
238@@ -887,37 +892,18 @@
239 See the comment in getDirectSubscribers for a description of the
240 recipients argument.
241 """
242- if self.private:
243- return []
244-
245 if level is None:
246- notification_level = BugNotificationLevel.NOTHING
247- else:
248- notification_level = level
249-
250- dupe_details = dict(
251- Store.of(self).find(
252- (Person, Bug),
253- BugSubscription.person == Person.id,
254- BugSubscription.bug_id == Bug.id,
255- BugSubscription.bug_notification_level >= notification_level,
256- Bug.duplicateof == self.id))
257-
258- dupe_subscribers = set(dupe_details)
259-
260- # Direct and "also notified" subscribers take precedence over
261- # subscribers from dupes. Note that we don't supply recipients
262- # here because we are doing this to /remove/ subscribers.
263- dupe_subscribers -= set(self.getDirectSubscribers())
264- dupe_subscribers -= set(self.getAlsoNotifiedSubscribers())
265+ level = BugNotificationLevel.NOTHING
266+ info = self.getSubscriptionInfo(level)
267
268 if recipients is not None:
269- for subscriber in dupe_subscribers:
270+ # Pre-load duplicate bugs.
271+ list(self.duplicates)
272+ for subscription in info.duplicate_only_subscriptions:
273 recipients.addDupeSubscriber(
274- subscriber, dupe_details[subscriber])
275+ subscription.person, subscription.bug)
276
277- return sorted(
278- dupe_subscribers, key=operator.attrgetter("displayname"))
279+ return info.duplicate_only_subscriptions.subscribers.sorted
280
281 def getSubscribersForPerson(self, person):
282 """See `IBug."""
283@@ -2091,10 +2077,34 @@
284
285
286 class BugSubscriptionInfo:
287- """Represents bug subscription sets."""
288+ """Represents bug subscription sets.
289+
290+ The intention for this class is to encapsulate all calculations of
291+ subscriptions and subscribers for a bug. Some design considerations:
292+
293+ * Immutable.
294+
295+ * Set-based.
296+
297+ * Sets are cached.
298+
299+ * Usable with a *snapshot* of a bug. This is interesting for two reasons:
300+
301+ - Event subscribers commonly deal with snapshots. An instance of this
302+ class could be added to a custom snapshot so that multiple subscribers
303+ can share the information it contains.
304+
305+ - Use outside of the web request. A serialized snapshot could be used to
306+ calculate subscribers for a particular bug state. This could help us
307+ to move even more bug mail processing out of the web request.
308+
309+ """
310+
311+ implements(IHasBug)
312
313 def __init__(self, bug, level):
314 self.bug = bug
315+ assert level is not None
316 self.level = level
317
318 @cachedproperty
319@@ -2120,6 +2130,22 @@
320 Bug.duplicateof == self.bug)
321
322 @cachedproperty
323+ @freeze(BugSubscriptionSet)
324+ def duplicate_only_subscriptions(self):
325+ """Subscripitions to duplicates of the bug.
326+
327+ Excludes subscriptions for people who have a direct subscription or
328+ are also notified for another reason.
329+ """
330+ self.duplicate_subscriptions.subscribers # Pre-load subscribers.
331+ higher_precedence = (
332+ self.direct_subscriptions.subscribers.union(
333+ self.also_notified_subscribers))
334+ return (
335+ subscription for subscription in self.duplicate_subscriptions
336+ if subscription.person not in higher_precedence)
337+
338+ @cachedproperty
339 @freeze(StructuralSubscriptionSet)
340 def structural_subscriptions(self):
341 """Structural subscriptions to the bug's targets."""
342@@ -2138,13 +2164,14 @@
343 if bugtask.milestone is not None:
344 query_arguments.append((bugtask.milestone, bugtask))
345 # Build the query.
346+ empty = EmptyResultSet()
347 union = lambda left, right: (
348 removeSecurityProxy(left).union(
349 removeSecurityProxy(right)))
350 queries = (
351 target.getSubscriptionsForBugTask(bugtask, self.level)
352 for target, bugtask in query_arguments)
353- return reduce(union, queries)
354+ return reduce(union, queries, empty)
355
356 @cachedproperty
357 @freeze(BugSubscriberSet)
358
359=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
360--- lib/lp/bugs/model/tests/test_bug.py 2010-11-12 18:05:45 +0000
361+++ lib/lp/bugs/model/tests/test_bug.py 2011-02-02 13:33:41 +0000
362@@ -4,6 +4,7 @@
363 __metaclass__ = type
364
365 from canonical.testing.layers import DatabaseFunctionalLayer
366+from lp.bugs.model.bug import BugSubscriptionInfo
367 from lp.registry.enum import BugNotificationLevel
368 from lp.registry.interfaces.person import PersonVisibility
369 from lp.registry.model.structuralsubscription import StructuralSubscription
370@@ -200,22 +201,16 @@
371 # the results retuned by getSubscribersFromDuplicates()
372 duplicate_bug.unsubscribe(
373 duplicate_bug.owner, duplicate_bug.owner)
374- reversed_levels = sorted(
375- BugNotificationLevel.items, reverse=True)
376- subscribers = []
377- for level in reversed_levels:
378+ for level in BugNotificationLevel.items:
379 subscriber = self.factory.makePerson()
380- subscribers.append(subscriber)
381 with person_logged_in(subscriber):
382 duplicate_bug.subscribe(subscriber, subscriber, level=level)
383- duplicate_subscribers = (
384+ # Only the most recently subscribed person will be included
385+ # because the previous subscribers are subscribed at a lower
386+ # level.
387+ self.assertEqual(
388+ (subscriber,),
389 bug.getSubscribersFromDuplicates(level=level))
390- # All the previous subscribers will be included because
391- # their level of subscription is such that they also receive
392- # notifications at the current level.
393- self.assertEqual(
394- set(subscribers), set(duplicate_subscribers),
395- "Number of subscribers did not match expected value.")
396
397 def test_subscribers_from_dupes_overrides_using_level(self):
398 # Bug.getSubscribersFromDuplicates() does not return subscribers
399@@ -240,3 +235,16 @@
400 self.assertTrue(
401 subscriber not in duplicate_subscribers,
402 "Subscriber should not be in duplicate_subscribers.")
403+
404+ def test_getSubscriptionInfo(self):
405+ # getSubscriptionInfo() returns a BugSubscriptionInfo object.
406+ bug = self.factory.makeBug()
407+ with person_logged_in(bug.owner):
408+ info = bug.getSubscriptionInfo()
409+ self.assertIsInstance(info, BugSubscriptionInfo)
410+ self.assertEqual(bug, info.bug)
411+ self.assertEqual(BugNotificationLevel.NOTHING, info.level)
412+ # A level can also be specified.
413+ with person_logged_in(bug.owner):
414+ info = bug.getSubscriptionInfo(BugNotificationLevel.METADATA)
415+ self.assertEqual(BugNotificationLevel.METADATA, info.level)
416
417=== modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py'
418--- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2010-12-06 20:58:55 +0000
419+++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2011-02-02 13:33:41 +0000
420@@ -9,7 +9,10 @@
421
422 from storm.store import Store
423 from testtools.matchers import Equals
424+from zope.component import queryAdapter
425+from zope.security.checker import getChecker
426
427+from canonical.launchpad.webapp.interfaces import IAuthorization
428 from canonical.testing.layers import DatabaseFunctionalLayer
429 from lp.bugs.model.bug import (
430 BugSubscriberSet,
431@@ -18,6 +21,9 @@
432 load_people,
433 StructuralSubscriptionSet,
434 )
435+from lp.bugs.security import (
436+ PublicToAllOrPrivateToExplicitSubscribersForBugTask,
437+ )
438 from lp.registry.enum import BugNotificationLevel
439 from lp.registry.model.person import Person
440 from lp.testing import (
441@@ -182,6 +188,28 @@
442 self.assertEqual(set(), found_subscriptions.subscribers)
443 self.assertEqual((), found_subscriptions.subscribers.sorted)
444
445+ def test_duplicate_only(self):
446+ # The set of duplicate subscriptions where the subscriber has no other
447+ # subscriptions.
448+ duplicate_bug = self.factory.makeBug(product=self.target)
449+ with person_logged_in(duplicate_bug.owner):
450+ duplicate_bug.markAsDuplicate(self.bug)
451+ duplicate_bug_subscription = (
452+ duplicate_bug.getSubscriptionForPerson(
453+ duplicate_bug.owner))
454+ found_subscriptions = self.getInfo().duplicate_only_subscriptions
455+ self.assertEqual(
456+ set([duplicate_bug_subscription]),
457+ found_subscriptions)
458+ # If a user is subscribed to a duplicate bug and is a bugtask
459+ # assignee, for example, their duplicate subscription will not be
460+ # included.
461+ with person_logged_in(self.target.owner):
462+ self.bug.default_bugtask.transitionToAssignee(
463+ duplicate_bug_subscription.person)
464+ found_subscriptions = self.getInfo().duplicate_only_subscriptions
465+ self.assertEqual(set(), found_subscriptions)
466+
467 def test_structural(self):
468 # The set of structural subscribers.
469 subscribers = (
470@@ -309,6 +337,30 @@
471 found_subscribers.sorted)
472
473
474+class TestBugSubscriptionInfoPermissions(TestCaseWithFactory):
475+
476+ layer = DatabaseFunctionalLayer
477+
478+ def test(self):
479+ bug = self.factory.makeBug()
480+ info = bug.getSubscriptionInfo()
481+ checker = getChecker(info)
482+
483+ # BugSubscriptionInfo objects are immutable.
484+ self.assertEqual({}, checker.set_permissions)
485+
486+ # All attributes require launchpad.View.
487+ permissions = set(checker.get_permissions.itervalues())
488+ self.assertEqual(set(["launchpad.View"]), permissions)
489+
490+ # The security adapter for launchpad.View lets anyone reference the
491+ # attributes unless the bug is private, in which case only explicit
492+ # subscribers are permitted.
493+ adapter = queryAdapter(info, IAuthorization, "launchpad.View")
494+ self.assertIsInstance(
495+ adapter, PublicToAllOrPrivateToExplicitSubscribersForBugTask)
496+
497+
498 class TestBugSubscriptionInfoQueries(TestCaseWithFactory):
499
500 layer = DatabaseFunctionalLayer