Merge lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email into lp:launchpad
- bugnomination-timeout-bug-874250-preload-email
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gavin Panella | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 14638 | ||||
Proposed branch: | lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
1541 lines (+706/-257) 9 files modified
lib/lp/bugs/configure.zcml (+13/-0) lib/lp/bugs/doc/bugsubscription.txt (+4/-4) lib/lp/bugs/interfaces/bug.py (+3/-2) lib/lp/bugs/interfaces/bugtask.py (+1/-1) lib/lp/bugs/model/bug.py (+180/-100) lib/lp/bugs/model/structuralsubscription.py (+84/-41) lib/lp/bugs/model/tests/test_bug.py (+82/-8) lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py (+239/-99) lib/lp/bugs/tests/test_structuralsubscription.py (+100/-2) |
||||
To merge this branch: | bzr merge lp:~allenap/launchpad/bugnomination-timeout-bug-874250-preload-email | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+87397@code.launchpad.net |
Commit message
Load preferredemail when calculating bug subscribers of any kind via BugSubscription
Description of the change
This branch is long, but is actually quite readable, I think :)
One big problem when updating bugs is sending notifications to
subscribers. It looks like preferred email addresses are being loaded
one by one (though I started this branch long enough ago that the
details have faded).
In getting email addresses preloaded I updated BugSubscription
fixed some issues with it, and gotten get_also_
using it.
I think BSI is now a fairly good and comprehensive foundation for any
bug subscription calculation. It may not do everything in the minimum
number of queries possible, but it's does everything in a constant
number of queries, and makes it easy for code that uses it to also do
so.
There are still methods in IBug (and probably elsewhere) that could be
refactored to use BSI more directly, but I'll leave that for another
time.
The changes:
- Updates BugSubscription
- Adds all_direct_
direct_
properties.
- Adds support for returning subscription info for only a single
bugtask as well as all bugtasks of the given bug.
- Adds forLevel() and forTask() methods.
- When loading Person records, load preferred email information too.
- Updates get_also_
- Use BugSubscriptionInfo more heavily.
- Adds tests around performance of this function when passed a
recipients argument. This was previously poor (potato potato).
- Updates structural subscriptions:
Split get_structural_
get_structura
which both back onto query_structura
function. This supports the changes to BugSubscriptionInfo and also
makes the implementation cleaner.
Gavin Panella (allenap) wrote : | # |
Jeroen T. Vermeulen (jtv) wrote : | # |
Hi Gavin,
This took me a while to review. By and large it looks nice, but I have a few cosmetic issues to rant on. Not all of them are vital, and some will seem more trouble than they're worth — unless they can become habits. It may seem petty of me; but I could give a more effective review without these things in the way.
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -99,7 +99,7 @@
> getAlsoNotified
>
> >>> linux_source_
> - [<Person at ...>]
> + (<Person at ...>,)
> >>> linux_source_
> ()
>
@@ -109,7 +109,7 @@
> >>> from lp.bugs.model.bug import get_also_
> >>> res = get_also_
> >>> res
> - [<Person at ...>]
> + (<Person at ...>,)
These two original tests were brittle and a bit obscure. They seemed to establish that (1) a list is returned and (2) the list contains one or more Persons. I suspect (1) is of no significance.
Your new tests establish that (1) a tuple is returned and (2) the tuple contains exactly one Person, as implied by the trailing comma. So you test more, but it's still brittle and still a bit obscure.
It's not worth spending too much time on any given instance of these problems, but it's good to have fixes ready when you come across them: listify so that it won't matter what kind of iterable you get, or print just the length, or use a “[foo] = give_me_
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -948,9 +949,10 @@
> BugSubscription
> return DecoratedResult
>
> - def getSubscription
> + def getSubscription
> """See `IBug`."""
> - return BugSubscription
> + return BugSubscription
> + self, BugNotification
Found the "if/else" slightly hard to read here. Maybe I'm just not used to the syntax yet, but it made me double-check that nothing else goes on in this line. Please consider putting the if/else on a separate line.
@@ -1002,6 +1004,8 @@
> # the regular proxied object.
> return sorted(
> indirect_
> + # XXX: GavinPanella 2011-12-12 bug=???: Should probably use
> + # person_sort_key.
> key=lambda x: removeSecurityP
Don't forget to file that bug!
@@ -2389,48 +2392,44 @@
> - # Direct subscriptions always take precedence over indirect
> - # subscriptions.
> - direct_
Gavin Panella (allenap) wrote : | # |
> Hi Gavin,
>
> This took me a while to review. By and large it looks nice, but I
> have a few cosmetic issues to rant on. Not all of them are vital,
> and some will seem more trouble than they're worth — unless they can
> become habits. It may seem petty of me; but I could give a more
> effective review without these things in the way.
Thanks for going through this branch!
>
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -99,7 +99,7 @@
> > getAlsoNotified
> >
> > >>> linux_source_
> > - [<Person at ...>]
> > + (<Person at ...>,)
> > >>> linux_source_
> > ()
> >
> @@ -109,7 +109,7 @@
> > >>> from lp.bugs.model.bug import get_also_
> > >>> res = get_also_
> > >>> res
> > - [<Person at ...>]
> > + (<Person at ...>,)
>
> These two original tests were brittle and a bit obscure. They
> seemed to establish that (1) a list is returned and (2) the list
> contains one or more Persons. I suspect (1) is of no significance.
>
> Your new tests establish that (1) a tuple is returned and (2) the
> tuple contains exactly one Person, as implied by the trailing comma.
> So you test more, but it's still brittle and still a bit obscure.
>
> It's not worth spending too much time on any given instance of these
> problems, but it's good to have fixes ready when you come across
> them: listify so that it won't matter what kind of iterable you get,
> or print just the length, or use a “[foo] =
> give_me_
> is what the test means to prove and what is incidental. Stay close
> to the former and away from the latter.
Good points, well put. I've amended those.
>
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
>
> @@ -948,9 +949,10 @@
> > BugSubscription
> > return DecoratedResult
> >
> > - def getSubscription
> > + def getSubscription
> > """See `IBug`."""
> > - return BugSubscription
> > + return BugSubscription
> > + self, BugNotification
> level)
>
> Found the "if/else" slightly hard to read here. Maybe I'm just not
> used to the syntax yet, but it made me double-check that nothing
> else goes on in this line. Please consider putting the if/else on a
> separate line.
Done.
>
>
> @@ -1002,6 +1004,8 @@
> > # the regular proxied object.
> > return sorted(
> > indirect_
> > + # XXX: GavinPanella 2011-12-12 bug=???: Should probably use
> > + # person_sort_key.
> ...
Preview Diff
1 | === modified file 'lib/lp/bugs/configure.zcml' |
2 | --- lib/lp/bugs/configure.zcml 2011-12-30 08:03:42 +0000 |
3 | +++ lib/lp/bugs/configure.zcml 2012-01-04 18:01:30 +0000 |
4 | @@ -650,6 +650,7 @@ |
5 | permission="launchpad.View" |
6 | attributes=" |
7 | bug |
8 | + bugtask |
9 | level |
10 | " /> |
11 | <!-- Properties --> |
12 | @@ -659,12 +660,24 @@ |
13 | all_assignees |
14 | all_pillar_owners_without_bug_supervisors |
15 | also_notified_subscribers |
16 | + direct_subscribers |
17 | + direct_subscribers_at_all_levels |
18 | direct_subscriptions |
19 | + direct_subscriptions_at_all_levels |
20 | duplicate_only_subscriptions |
21 | duplicate_subscriptions |
22 | indirect_subscribers |
23 | + muted_subscribers |
24 | + structural_subscribers |
25 | structural_subscriptions |
26 | " /> |
27 | + <!-- Methods --> |
28 | + <require |
29 | + permission="launchpad.View" |
30 | + attributes=" |
31 | + forLevel |
32 | + forTask |
33 | + " /> |
34 | </class> |
35 | |
36 | <!-- PersonSubscriptionInfo --> |
37 | |
38 | === modified file 'lib/lp/bugs/doc/bugsubscription.txt' |
39 | --- lib/lp/bugs/doc/bugsubscription.txt 2011-12-24 17:49:30 +0000 |
40 | +++ lib/lp/bugs/doc/bugsubscription.txt 2012-01-04 18:01:30 +0000 |
41 | @@ -98,17 +98,17 @@ |
42 | Finer-grained access to indirect subscribers is provided by |
43 | getAlsoNotifiedSubscribers() and getSubscribersFromDuplicates(). |
44 | |
45 | - >>> linux_source_bug.getAlsoNotifiedSubscribers() |
46 | + >>> list(linux_source_bug.getAlsoNotifiedSubscribers()) |
47 | [<Person at ...>] |
48 | - >>> linux_source_bug.getSubscribersFromDuplicates() |
49 | - () |
50 | + >>> list(linux_source_bug.getSubscribersFromDuplicates()) |
51 | + [] |
52 | |
53 | It is also possible to get the list of indirect subscribers for an |
54 | individual bug task. |
55 | |
56 | >>> from lp.bugs.model.bug import get_also_notified_subscribers |
57 | >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0]) |
58 | - >>> res |
59 | + >>> list(res) |
60 | [<Person at ...>] |
61 | |
62 | These are security proxied. |
63 | |
64 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
65 | --- lib/lp/bugs/interfaces/bug.py 2012-01-01 02:58:52 +0000 |
66 | +++ lib/lp/bugs/interfaces/bug.py 2012-01-04 18:01:30 +0000 |
67 | @@ -578,10 +578,11 @@ |
68 | If no such `BugSubscription` exists, return None. |
69 | """ |
70 | |
71 | - def getSubscriptionInfo(level): |
72 | + def getSubscriptionInfo(level=None): |
73 | """Return a `BugSubscriptionInfo` at the given `level`. |
74 | |
75 | - :param level: A member of `BugNotificationLevel`. |
76 | + :param level: A member of `BugNotificationLevel`. Defaults to |
77 | + `BugSubscriptionLevel.LIFECYCLE` if unspecified. |
78 | """ |
79 | |
80 | def getBugNotificationRecipients(duplicateof=None, old_bug=None, |
81 | |
82 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
83 | --- lib/lp/bugs/interfaces/bugtask.py 2012-01-01 02:58:52 +0000 |
84 | +++ lib/lp/bugs/interfaces/bugtask.py 2012-01-04 18:01:30 +0000 |
85 | @@ -560,7 +560,7 @@ |
86 | title=_('Assigned to'), required=False, |
87 | vocabulary='ValidAssignee', |
88 | readonly=True)) |
89 | - assigneeID = Attribute('The assignee ID (for eager loading)') |
90 | + assigneeID = Int(title=_('The assignee ID (for eager loading)')) |
91 | bugtargetdisplayname = exported( |
92 | Text(title=_("The short, descriptive name of the target"), |
93 | readonly=True), |
94 | |
95 | === modified file 'lib/lp/bugs/model/bug.py' |
96 | --- lib/lp/bugs/model/bug.py 2011-12-30 06:14:56 +0000 |
97 | +++ lib/lp/bugs/model/bug.py 2012-01-04 18:01:30 +0000 |
98 | @@ -158,6 +158,7 @@ |
99 | from lp.bugs.model.bugwatch import BugWatch |
100 | from lp.bugs.model.structuralsubscription import ( |
101 | get_structural_subscribers, |
102 | + get_structural_subscriptions, |
103 | get_structural_subscriptions_for_bug, |
104 | ) |
105 | from lp.code.interfaces.branchcollection import IAllBranches |
106 | @@ -948,8 +949,10 @@ |
107 | BugSubscription.bug_id == self.id).order_by(BugSubscription.id) |
108 | return DecoratedResultSet(results, operator.itemgetter(1)) |
109 | |
110 | - def getSubscriptionInfo(self, level=BugNotificationLevel.LIFECYCLE): |
111 | + def getSubscriptionInfo(self, level=None): |
112 | """See `IBug`.""" |
113 | + if level is None: |
114 | + level = BugNotificationLevel.LIFECYCLE |
115 | return BugSubscriptionInfo(self, level) |
116 | |
117 | def getDirectSubscriptions(self): |
118 | @@ -1002,6 +1005,7 @@ |
119 | # the regular proxied object. |
120 | return sorted( |
121 | indirect_subscribers, |
122 | + # XXX: GavinPanella 2011-12-12 bug=911752: Use person_sort_key. |
123 | key=lambda x: removeSecurityProxy(x).displayname) |
124 | |
125 | def getSubscriptionsFromDuplicates(self, recipients=None): |
126 | @@ -1030,14 +1034,13 @@ |
127 | if level is None: |
128 | level = BugNotificationLevel.LIFECYCLE |
129 | info = self.getSubscriptionInfo(level) |
130 | - |
131 | if recipients is not None: |
132 | - # Pre-load duplicate bugs. |
133 | - list(self.duplicates) |
134 | + list(self.duplicates) # Pre-load duplicate bugs. |
135 | + info.duplicate_only_subscribers # Pre-load subscribers. |
136 | for subscription in info.duplicate_only_subscriptions: |
137 | recipients.addDupeSubscriber( |
138 | subscription.person, subscription.bug) |
139 | - return info.duplicate_only_subscriptions.subscribers.sorted |
140 | + return info.duplicate_only_subscribers.sorted |
141 | |
142 | def getSubscribersForPerson(self, person): |
143 | """See `IBug.""" |
144 | @@ -2389,48 +2392,44 @@ |
145 | if IBug.providedBy(bug_or_bugtask): |
146 | bug = bug_or_bugtask |
147 | bugtasks = bug.bugtasks |
148 | + info = bug.getSubscriptionInfo(level) |
149 | elif IBugTask.providedBy(bug_or_bugtask): |
150 | bug = bug_or_bugtask.bug |
151 | bugtasks = [bug_or_bugtask] |
152 | + info = bug.getSubscriptionInfo(level).forTask(bug_or_bugtask) |
153 | else: |
154 | raise ValueError('First argument must be bug or bugtask') |
155 | |
156 | if bug.private: |
157 | return [] |
158 | |
159 | - # Direct subscriptions always take precedence over indirect |
160 | - # subscriptions. |
161 | - direct_subscribers = set(bug.getDirectSubscribers()) |
162 | - |
163 | - also_notified_subscribers = set() |
164 | - |
165 | - for bugtask in bugtasks: |
166 | - if (bugtask.assignee and |
167 | - bugtask.assignee not in direct_subscribers): |
168 | - # We have an assignee that is not a direct subscriber. |
169 | - also_notified_subscribers.add(bugtask.assignee) |
170 | - if recipients is not None: |
171 | + # Subscribers to exclude. |
172 | + exclude_subscribers = frozenset().union( |
173 | + info.direct_subscribers_at_all_levels, info.muted_subscribers) |
174 | + # Get also-notified subscribers at the given level for the given tasks. |
175 | + also_notified_subscribers = info.also_notified_subscribers |
176 | + |
177 | + if recipients is not None: |
178 | + for bugtask in bugtasks: |
179 | + assignee = bugtask.assignee |
180 | + if assignee in also_notified_subscribers: |
181 | + # We have an assignee that is not a direct subscriber. |
182 | recipients.addAssignee(bugtask.assignee) |
183 | - |
184 | - # If the target's bug supervisor isn't set... |
185 | - pillar = bugtask.pillar |
186 | - if (pillar.bug_supervisor is None and |
187 | - pillar.official_malone and |
188 | - pillar.owner not in direct_subscribers): |
189 | - # ...we add the owner as a subscriber. |
190 | - also_notified_subscribers.add(pillar.owner) |
191 | - if recipients is not None: |
192 | - recipients.addRegistrant(pillar.owner, pillar) |
193 | + # If the target's bug supervisor isn't set... |
194 | + pillar = bugtask.pillar |
195 | + if pillar.official_malone and pillar.bug_supervisor is None: |
196 | + if pillar.owner in also_notified_subscribers: |
197 | + # ...we add the owner as a subscriber. |
198 | + recipients.addRegistrant(pillar.owner, pillar) |
199 | |
200 | # This structural subscribers code omits direct subscribers itself. |
201 | - also_notified_subscribers.update( |
202 | - get_structural_subscribers( |
203 | - bug_or_bugtask, recipients, level, direct_subscribers)) |
204 | + # TODO: Pass the info object into get_structural_subscribers for |
205 | + # efficiency... or do the recipients stuff here. |
206 | + structural_subscribers = get_structural_subscribers( |
207 | + bug_or_bugtask, recipients, level, exclude_subscribers) |
208 | + assert also_notified_subscribers.issuperset(structural_subscribers) |
209 | |
210 | - # Remove security proxy for the sort key, but return |
211 | - # the regular proxied object. |
212 | - return sorted(also_notified_subscribers, |
213 | - key=lambda x: removeSecurityProxy(x).displayname) |
214 | + return also_notified_subscribers.sorted |
215 | |
216 | |
217 | def load_people(*where): |
218 | @@ -2443,7 +2442,8 @@ |
219 | `ValidPersonCache` records are loaded simultaneously. |
220 | """ |
221 | return PersonSet()._getPrecachedPersons( |
222 | - origin=[Person], conditions=where, need_validity=True) |
223 | + origin=[Person], conditions=where, need_validity=True, |
224 | + need_preferred_email=True) |
225 | |
226 | |
227 | class BugSubscriberSet(frozenset): |
228 | @@ -2573,13 +2573,57 @@ |
229 | |
230 | def __init__(self, bug, level): |
231 | self.bug = bug |
232 | + self.bugtask = None # Implies all. |
233 | assert level is not None |
234 | self.level = level |
235 | + # This cache holds related `BugSubscriptionInfo` instances relating to |
236 | + # the same bug but with different levels and/or choice of bugtask. |
237 | + self.cache = {self.cache_key: self} |
238 | + # This is often used in event handlers, many of which block implicit |
239 | + # flushes. However, the data needs to be in the database for the |
240 | + # queries herein to give correct answers. |
241 | + Store.of(bug).flush() |
242 | + |
243 | + @property |
244 | + def cache_key(self): |
245 | + """A (bug ID, bugtask ID, level) tuple for use as a hash key. |
246 | + |
247 | + This helps `forTask()` and `forLevel()` to be more efficient, |
248 | + returning previously populated instances to avoid running the same |
249 | + queries against the database again and again. |
250 | + """ |
251 | + bugtask_id = None if self.bugtask is None else self.bugtask.id |
252 | + return self.bug.id, bugtask_id, self.level |
253 | + |
254 | + def forTask(self, bugtask): |
255 | + """Create a new `BugSubscriptionInfo` limited to `bugtask`. |
256 | + |
257 | + The given task must refer to this object's bug. If `None` is passed a |
258 | + new `BugSubscriptionInfo` instance is returned with no limit. |
259 | + """ |
260 | + info = self.__class__(self.bug, self.level) |
261 | + info.bugtask, info.cache = bugtask, self.cache |
262 | + return self.cache.setdefault(info.cache_key, info) |
263 | + |
264 | + def forLevel(self, level): |
265 | + """Create a new `BugSubscriptionInfo` limited to `level`.""" |
266 | + info = self.__class__(self.bug, level) |
267 | + info.bugtask, info.cache = self.bugtask, self.cache |
268 | + return self.cache.setdefault(info.cache_key, info) |
269 | + |
270 | + @cachedproperty |
271 | + @freeze(BugSubscriberSet) |
272 | + def muted_subscribers(self): |
273 | + muted_people = Select(BugMute.person_id, BugMute.bug == self.bug) |
274 | + return load_people(Person.id.is_in(muted_people)) |
275 | |
276 | @cachedproperty |
277 | @freeze(BugSubscriptionSet) |
278 | - def old_direct_subscriptions(self): |
279 | - """The bug's direct subscriptions.""" |
280 | + def direct_subscriptions(self): |
281 | + """The bug's direct subscriptions. |
282 | + |
283 | + Excludes muted subscriptions. |
284 | + """ |
285 | return IStore(BugSubscription).find( |
286 | BugSubscription, |
287 | BugSubscription.bug_notification_level >= self.level, |
288 | @@ -2587,66 +2631,64 @@ |
289 | Not(In(BugSubscription.person_id, |
290 | Select(BugMute.person_id, BugMute.bug_id == self.bug.id)))) |
291 | |
292 | - @cachedproperty |
293 | - def direct_subscriptions_and_subscribers(self): |
294 | - """The bug's direct subscriptions.""" |
295 | - res = IStore(BugSubscription).find( |
296 | - (BugSubscription, Person), |
297 | - BugSubscription.bug_notification_level >= self.level, |
298 | - BugSubscription.bug == self.bug, |
299 | - BugSubscription.person_id == Person.id, |
300 | - Not(In(BugSubscription.person_id, |
301 | - Select(BugMute.person_id, |
302 | - BugMute.bug_id == self.bug.id)))) |
303 | - # Here we could test for res.count() but that will execute another |
304 | - # query. This structure avoids the extra query. |
305 | - return zip(*res) or ((), ()) |
306 | - |
307 | - @cachedproperty |
308 | - @freeze(BugSubscriptionSet) |
309 | - def direct_subscriptions(self): |
310 | - return self.direct_subscriptions_and_subscribers[0] |
311 | - |
312 | - @cachedproperty |
313 | - @freeze(BugSubscriberSet) |
314 | + @property |
315 | def direct_subscribers(self): |
316 | - return self.direct_subscriptions_and_subscribers[1] |
317 | + """The bug's direct subscriptions. |
318 | + |
319 | + Excludes muted subscribers. |
320 | + """ |
321 | + return self.direct_subscriptions.subscribers |
322 | + |
323 | + @property |
324 | + def direct_subscriptions_at_all_levels(self): |
325 | + """The bug's direct subscriptions at all levels. |
326 | + |
327 | + Excludes muted subscriptions. |
328 | + """ |
329 | + return self.forLevel( |
330 | + BugNotificationLevel.LIFECYCLE).direct_subscriptions |
331 | + |
332 | + @property |
333 | + def direct_subscribers_at_all_levels(self): |
334 | + """The bug's direct subscribers at all levels. |
335 | + |
336 | + Excludes muted subscribers. |
337 | + """ |
338 | + return self.direct_subscriptions_at_all_levels.subscribers |
339 | |
340 | @cachedproperty |
341 | - def duplicate_subscriptions_and_subscribers(self): |
342 | - """Subscriptions to duplicates of the bug.""" |
343 | + @freeze(BugSubscriptionSet) |
344 | + def duplicate_subscriptions(self): |
345 | + """Subscriptions to duplicates of the bug. |
346 | + |
347 | + Excludes muted subscriptions. |
348 | + """ |
349 | if self.bug.private: |
350 | - return ((), ()) |
351 | + return () |
352 | else: |
353 | - res = IStore(BugSubscription).find( |
354 | - (BugSubscription, Person), |
355 | + return IStore(BugSubscription).find( |
356 | + BugSubscription, |
357 | BugSubscription.bug_notification_level >= self.level, |
358 | BugSubscription.bug_id == Bug.id, |
359 | - BugSubscription.person_id == Person.id, |
360 | Bug.duplicateof == self.bug, |
361 | Not(In(BugSubscription.person_id, |
362 | Select(BugMute.person_id, BugMute.bug_id == Bug.id)))) |
363 | - # Here we could test for res.count() but that will execute another |
364 | - # query. This structure avoids the extra query. |
365 | - return zip(*res) or ((), ()) |
366 | - |
367 | - @cachedproperty |
368 | - @freeze(BugSubscriptionSet) |
369 | - def duplicate_subscriptions(self): |
370 | - return self.duplicate_subscriptions_and_subscribers[0] |
371 | - |
372 | - @cachedproperty |
373 | - @freeze(BugSubscriberSet) |
374 | + |
375 | + @property |
376 | def duplicate_subscribers(self): |
377 | - return self.duplicate_subscriptions_and_subscribers[1] |
378 | + """Subscribers to duplicates of the bug. |
379 | + |
380 | + Excludes muted subscribers. |
381 | + """ |
382 | + return self.duplicate_subscriptions.subscribers |
383 | |
384 | @cachedproperty |
385 | @freeze(BugSubscriptionSet) |
386 | def duplicate_only_subscriptions(self): |
387 | - """Subscriptions to duplicates of the bug. |
388 | + """Subscriptions to duplicates of the bug only. |
389 | |
390 | - Excludes subscriptions for people who have a direct subscription or |
391 | - are also notified for another reason. |
392 | + Excludes muted subscriptions, subscriptions for people who have a |
393 | + direct subscription, or who are also notified for another reason. |
394 | """ |
395 | self.duplicate_subscribers # Pre-load subscribers. |
396 | higher_precedence = ( |
397 | @@ -2656,47 +2698,85 @@ |
398 | subscription for subscription in self.duplicate_subscriptions |
399 | if subscription.person not in higher_precedence) |
400 | |
401 | + @property |
402 | + def duplicate_only_subscribers(self): |
403 | + """Subscribers to duplicates of the bug only. |
404 | + |
405 | + Excludes muted subscribers, subscribers who have a direct |
406 | + subscription, or who are also notified for another reason. |
407 | + """ |
408 | + return self.duplicate_only_subscriptions.subscribers |
409 | + |
410 | @cachedproperty |
411 | @freeze(StructuralSubscriptionSet) |
412 | def structural_subscriptions(self): |
413 | - """Structural subscriptions to the bug's targets.""" |
414 | - return list(get_structural_subscriptions_for_bug(self.bug)) |
415 | + """Structural subscriptions to the bug's targets. |
416 | + |
417 | + Excludes direct subscriptions. |
418 | + """ |
419 | + subject = self.bug if self.bugtask is None else self.bugtask |
420 | + return get_structural_subscriptions(subject, self.level) |
421 | + |
422 | + @property |
423 | + def structural_subscribers(self): |
424 | + """Structural subscribers to the bug's targets. |
425 | + |
426 | + Excludes direct subscribers. |
427 | + """ |
428 | + return self.structural_subscriptions.subscribers |
429 | |
430 | @cachedproperty |
431 | @freeze(BugSubscriberSet) |
432 | def all_assignees(self): |
433 | - """Assignees of the bug's tasks.""" |
434 | - assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug) |
435 | - return load_people(Person.id.is_in(assignees)) |
436 | + """Assignees of the bug's tasks. |
437 | + |
438 | + *Does not* exclude muted subscribers. |
439 | + """ |
440 | + if self.bugtask is None: |
441 | + assignees = Select(BugTask.assigneeID, BugTask.bug == self.bug) |
442 | + return load_people(Person.id.is_in(assignees)) |
443 | + else: |
444 | + return load_people(Person.id == self.bugtask.assigneeID) |
445 | |
446 | @cachedproperty |
447 | @freeze(BugSubscriberSet) |
448 | def all_pillar_owners_without_bug_supervisors(self): |
449 | - """Owners of pillars for which no Bug supervisor is configured.""" |
450 | - for bugtask in self.bug.bugtasks: |
451 | + """Owners of pillars for which there is no bug supervisor. |
452 | + |
453 | + The pillars must also use Launchpad for bug tracking. |
454 | + |
455 | + *Does not* exclude muted subscribers. |
456 | + """ |
457 | + if self.bugtask is None: |
458 | + bugtasks = self.bug.bugtasks |
459 | + else: |
460 | + bugtasks = [self.bugtask] |
461 | + for bugtask in bugtasks: |
462 | pillar = bugtask.pillar |
463 | - if pillar.bug_supervisor is None: |
464 | - yield pillar.owner |
465 | + if pillar.official_malone: |
466 | + if pillar.bug_supervisor is None: |
467 | + yield pillar.owner |
468 | |
469 | @cachedproperty |
470 | def also_notified_subscribers(self): |
471 | - """All subscribers except direct and dupe subscribers.""" |
472 | + """All subscribers except direct, dupe, and muted subscribers.""" |
473 | if self.bug.private: |
474 | return BugSubscriberSet() |
475 | else: |
476 | - muted = IStore(BugMute).find( |
477 | - Person, |
478 | - BugMute.person_id == Person.id, |
479 | - BugMute.bug == self.bug) |
480 | - return BugSubscriberSet().union( |
481 | - self.structural_subscriptions.subscribers, |
482 | + subscribers = BugSubscriberSet().union( |
483 | + self.structural_subscribers, |
484 | self.all_pillar_owners_without_bug_supervisors, |
485 | - self.all_assignees).difference( |
486 | - self.direct_subscribers).difference(muted) |
487 | + self.all_assignees) |
488 | + return subscribers.difference( |
489 | + self.direct_subscribers_at_all_levels, |
490 | + self.muted_subscribers) |
491 | |
492 | @cachedproperty |
493 | def indirect_subscribers(self): |
494 | - """All subscribers except direct subscribers.""" |
495 | + """All subscribers except direct subscribers. |
496 | + |
497 | + Excludes muted subscribers. |
498 | + """ |
499 | return self.also_notified_subscribers.union( |
500 | self.duplicate_subscribers) |
501 | |
502 | |
503 | === modified file 'lib/lp/bugs/model/structuralsubscription.py' |
504 | --- lib/lp/bugs/model/structuralsubscription.py 2011-12-30 06:14:56 +0000 |
505 | +++ lib/lp/bugs/model/structuralsubscription.py 2012-01-04 18:01:30 +0000 |
506 | @@ -3,10 +3,11 @@ |
507 | |
508 | __metaclass__ = type |
509 | __all__ = [ |
510 | + 'get_structural_subscribers', |
511 | + 'get_structural_subscription_targets', |
512 | + 'get_structural_subscriptions', |
513 | 'get_structural_subscriptions_for_bug', |
514 | 'get_structural_subscriptions_for_target', |
515 | - 'get_structural_subscribers', |
516 | - 'get_structural_subscription_targets', |
517 | 'StructuralSubscription', |
518 | 'StructuralSubscriptionTargetMixin', |
519 | ] |
520 | @@ -478,14 +479,14 @@ |
521 | def getSubscriptions(self, subscriber=None): |
522 | """See `IStructuralSubscriptionTarget`.""" |
523 | from lp.registry.model.person import Person |
524 | - clauses = [StructuralSubscription.subscriberID==Person.id] |
525 | + clauses = [StructuralSubscription.subscriberID == Person.id] |
526 | for key, value in self._target_args.iteritems(): |
527 | clauses.append( |
528 | - getattr(StructuralSubscription, key)==value) |
529 | + getattr(StructuralSubscription, key) == value) |
530 | |
531 | if subscriber is not None: |
532 | clauses.append( |
533 | - StructuralSubscription.subscriberID==subscriber.id) |
534 | + StructuralSubscription.subscriberID == subscriber.id) |
535 | |
536 | store = Store.of(self.__helper.pillar) |
537 | return store.find( |
538 | @@ -588,54 +589,96 @@ |
539 | *conditions) |
540 | |
541 | |
542 | -def get_structural_subscribers( |
543 | - bug_or_bugtask, recipients, level, direct_subscribers=None): |
544 | - """Return subscribers for bug or bugtask at level. |
545 | - |
546 | - :param bug: a bug. |
547 | - :param recipients: a BugNotificationRecipients object or None. |
548 | - Populates if given. |
549 | - :param level: a level from lp.bugs.enum.BugNotificationLevel. |
550 | - :param direct_subscribers: a collection of Person objects who are |
551 | - directly subscribed to the bug. |
552 | - |
553 | - Excludes structural subscriptions for people who are directly subscribed |
554 | - to the bug.""" |
555 | - if IBug.providedBy(bug_or_bugtask): |
556 | - bug = bug_or_bugtask |
557 | - bugtasks = bug.bugtasks |
558 | - elif IBugTask.providedBy(bug_or_bugtask): |
559 | - bug = bug_or_bugtask.bug |
560 | - bugtasks = [bug_or_bugtask] |
561 | - else: |
562 | - raise ValueError('First argument must be bug or bugtask') |
563 | +def query_structural_subscriptions( |
564 | + what, bug, bugtasks, level, exclude=None): |
565 | + """Query into structural subscriptions for a given bug. |
566 | + |
567 | + :param what: The fields to fetch. Choose from `Person`, |
568 | + `StructuralSubscription`, `BugSubscriptionFilter`, or a combo. |
569 | + :param bug: An `IBug` |
570 | + :param bugtasks: An iterable of `IBugTask`. |
571 | + :param level: A level from `BugNotificationLevel`. Filters below this |
572 | + level will be excluded. |
573 | + :param exclude: `Person`s to exclude (e.g. direct subscribers). |
574 | + """ |
575 | + from lp.registry.model.person import Person # Circular. |
576 | filter_id_query = ( |
577 | _get_structural_subscription_filter_id_query( |
578 | - bug, bugtasks, level, direct_subscribers)) |
579 | + bug, bugtasks, level, exclude)) |
580 | if not filter_id_query: |
581 | return EmptyResultSet() |
582 | - # This is here because of a circular import. |
583 | - from lp.registry.model.person import Person |
584 | source = IStore(StructuralSubscription).using( |
585 | StructuralSubscription, |
586 | Join(BugSubscriptionFilter, |
587 | BugSubscriptionFilter.structural_subscription_id == |
588 | StructuralSubscription.id), |
589 | Join(Person, |
590 | - Person.id == StructuralSubscription.subscriberID), |
591 | - ) |
592 | + Person.id == StructuralSubscription.subscriberID)) |
593 | + conditions = In( |
594 | + BugSubscriptionFilter.id, filter_id_query) |
595 | + return source.find(what, conditions) |
596 | + |
597 | + |
598 | +def get_bug_and_bugtasks(bug_or_bugtask): |
599 | + """Return a bug and a list of bugtasks given a bug or a bugtask. |
600 | + |
601 | + :param bug_or_bugtask: An `IBug` or `IBugTask`. |
602 | + :raises ValueError: If `bug_or_bugtask` does not provide `IBug` or |
603 | + `IBugTask`. |
604 | + """ |
605 | + if IBug.providedBy(bug_or_bugtask): |
606 | + return bug_or_bugtask, bug_or_bugtask.bugtasks |
607 | + elif IBugTask.providedBy(bug_or_bugtask): |
608 | + return bug_or_bugtask.bug, [bug_or_bugtask] |
609 | + else: |
610 | + raise ValueError( |
611 | + "Expected bug or bugtask, got %r" % (bug_or_bugtask,)) |
612 | + |
613 | + |
614 | +def get_structural_subscriptions(bug_or_bugtask, level, exclude=None): |
615 | + """Return subscriptions for bug or bugtask at level. |
616 | + |
617 | + :param bug_or_bugtask: An `IBug` or `IBugTask`. |
618 | + :param level: A level from `BugNotificationLevel`. Filters below this |
619 | + level will be excluded. |
620 | + :param exclude: `Person`s to exclude (e.g. direct subscribers). |
621 | + """ |
622 | + from lp.registry.model.person import Person # Circular. |
623 | + bug, bugtasks = get_bug_and_bugtasks(bug_or_bugtask) |
624 | + subscriptions = query_structural_subscriptions( |
625 | + StructuralSubscription, bug, bugtasks, level, exclude) |
626 | + # Return only the first subscription and filter per subscriber. |
627 | + subscriptions.config(distinct=(Person.id,)) |
628 | + subscriptions.order_by( |
629 | + Person.id, StructuralSubscription.id, |
630 | + BugSubscriptionFilter.id) |
631 | + return subscriptions |
632 | + |
633 | + |
634 | +def get_structural_subscribers( |
635 | + bug_or_bugtask, recipients, level, exclude=None): |
636 | + """Return subscribers for bug or bugtask at level. |
637 | + |
638 | + :param bug_or_bugtask: An `IBug` or `IBugTask`. |
639 | + :param recipients: A `BugNotificationRecipients` object or |
640 | + `None`, which will be populated if provided. |
641 | + :param level: A level from `BugNotificationLevel`. Filters below this |
642 | + level will be excluded. |
643 | + :param exclude: `Person`s to exclude (e.g. direct subscribers). |
644 | + """ |
645 | + from lp.registry.model.person import Person # Circular. |
646 | + bug, bugtasks = get_bug_and_bugtasks(bug_or_bugtask) |
647 | if recipients is None: |
648 | - return source.find( |
649 | - Person, |
650 | - In(BugSubscriptionFilter.id, |
651 | - filter_id_query)).config(distinct=True).order_by() |
652 | + subscribers = query_structural_subscriptions( |
653 | + Person, bug, bugtasks, level, exclude) |
654 | + subscribers.config(distinct=True) |
655 | + return subscribers.order_by() |
656 | else: |
657 | + results = query_structural_subscriptions( |
658 | + (Person, StructuralSubscription, BugSubscriptionFilter), |
659 | + bug, bugtasks, level, exclude) |
660 | subscribers = [] |
661 | - query_results = source.find( |
662 | - (Person, StructuralSubscription, BugSubscriptionFilter), |
663 | - In(BugSubscriptionFilter.id, filter_id_query)) |
664 | - for person, subscription, filter in query_results: |
665 | - # Set up results. |
666 | + for person, subscription, filter in results: |
667 | if person not in recipients: |
668 | subscribers.append(person) |
669 | recipients.addStructuralSubscriber( |
670 | @@ -839,7 +882,7 @@ |
671 | group_by=(BugSubscriptionFilter.id,), |
672 | having=Count( |
673 | SQL('CASE WHEN BugSubscriptionFilterTag.include ' |
674 | - 'THEN BugSubscriptionFilterTag.tag END'))==0) |
675 | + 'THEN BugSubscriptionFilterTag.tag END')) == 0) |
676 | else: |
677 | # The bug has some tags. This will require a bit of fancy |
678 | # footwork. First, though, we will simply want to leave out |
679 | |
680 | === modified file 'lib/lp/bugs/model/tests/test_bug.py' |
681 | --- lib/lp/bugs/model/tests/test_bug.py 2012-01-01 02:58:52 +0000 |
682 | +++ lib/lp/bugs/model/tests/test_bug.py 2012-01-04 18:01:30 +0000 |
683 | @@ -22,6 +22,7 @@ |
684 | from lp.bugs.errors import BugCannotBePrivate |
685 | from lp.bugs.interfaces.bugnotification import IBugNotificationSet |
686 | from lp.bugs.interfaces.bugtask import BugTaskStatus |
687 | +from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients |
688 | from lp.bugs.model.bug import ( |
689 | BugNotification, |
690 | BugSubscriptionInfo, |
691 | @@ -35,6 +36,7 @@ |
692 | feature_flags, |
693 | login_person, |
694 | person_logged_in, |
695 | + record_two_runs, |
696 | set_feature_flag, |
697 | StormStatementRecorder, |
698 | TestCaseWithFactory, |
699 | @@ -166,7 +168,7 @@ |
700 | with StormStatementRecorder() as recorder: |
701 | subscribers = list(bug.getDirectSubscribers()) |
702 | self.assertThat(len(subscribers), Equals(10 + 1)) |
703 | - self.assertThat(recorder, HasQueryCount(Equals(1))) |
704 | + self.assertThat(recorder, HasQueryCount(Equals(2))) |
705 | |
706 | def test_mark_as_duplicate_query_count(self): |
707 | bug = self.factory.makeBug() |
708 | @@ -476,6 +478,78 @@ |
709 | self.assertContentEqual(public_branches, linked_branches) |
710 | self.assertNotIn(private_branch, linked_branches) |
711 | |
712 | + def test_getDirectSubscribers_with_recipients_query_count(self): |
713 | + # getDirectSubscribers() uses a constant number of queries when given |
714 | + # a recipients argument regardless of the number of subscribers. |
715 | + bug = self.factory.makeBug() |
716 | + |
717 | + def create_subscriber(): |
718 | + subscriber = self.factory.makePerson() |
719 | + with person_logged_in(subscriber): |
720 | + bug.subscribe(subscriber, subscriber) |
721 | + |
722 | + def get_subscribers(): |
723 | + recipients = BugNotificationRecipients() |
724 | + subs = bug.getDirectSubscribers(recipients=recipients) |
725 | + list(subs) # Ensure they're pulled. |
726 | + |
727 | + recorder1, recorder2 = record_two_runs( |
728 | + get_subscribers, create_subscriber, 3) |
729 | + self.assertThat( |
730 | + recorder2, HasQueryCount(Equals(recorder1.count))) |
731 | + |
732 | + def test_getSubscribersFromDuplicates_with_recipients_query_count(self): |
733 | + # getSubscribersFromDuplicates() uses a constant number of queries |
734 | + # when given a recipients argument regardless of the number of |
735 | + # subscribers. |
736 | + bug = self.factory.makeBug() |
737 | + duplicate_bug = self.factory.makeBug() |
738 | + with person_logged_in(duplicate_bug.owner): |
739 | + duplicate_bug.markAsDuplicate(bug) |
740 | + |
741 | + def create_subscriber(): |
742 | + subscriber = self.factory.makePerson() |
743 | + with person_logged_in(subscriber): |
744 | + duplicate_bug.subscribe(subscriber, subscriber) |
745 | + |
746 | + def get_subscribers(): |
747 | + recipients = BugNotificationRecipients() |
748 | + subs = bug.getSubscribersFromDuplicates(recipients=recipients) |
749 | + list(subs) # Ensure they're pulled. |
750 | + |
751 | + recorder1, recorder2 = record_two_runs( |
752 | + get_subscribers, create_subscriber, 3) |
753 | + self.assertThat( |
754 | + recorder2, HasQueryCount(Equals(recorder1.count))) |
755 | + |
756 | + def test_getAlsoNotifiedSubscribers_with_recipients_query_count(self): |
757 | + # getAlsoNotifiedSubscribers() uses a constant number of queries when |
758 | + # given a recipients argument regardless of the number of subscribers. |
759 | + bug = self.factory.makeBug() |
760 | + |
761 | + def create_stuff(): |
762 | + # Create a new bugtask, set its assignee, set its pillar's |
763 | + # official_malone=True, and subscribe someone to its target. |
764 | + bugtask = self.factory.makeBugTask(bug=bug) |
765 | + with person_logged_in(bugtask.owner): |
766 | + bugtask.transitionToAssignee(bugtask.owner) |
767 | + with person_logged_in(bugtask.pillar.owner): |
768 | + bugtask.pillar.official_malone = True |
769 | + subscriber = self.factory.makePerson() |
770 | + with person_logged_in(subscriber): |
771 | + bugtask.target.addSubscription( |
772 | + subscriber, subscriber) |
773 | + |
774 | + def get_subscribers(): |
775 | + recipients = BugNotificationRecipients() |
776 | + subs = bug.getAlsoNotifiedSubscribers(recipients=recipients) |
777 | + list(subs) # Ensure they're pulled. |
778 | + |
779 | + recorder1, recorder2 = record_two_runs( |
780 | + get_subscribers, create_stuff, 3) |
781 | + self.assertThat( |
782 | + recorder2, HasQueryCount(Equals(recorder1.count))) |
783 | + |
784 | |
785 | class TestBugPrivateAndSecurityRelatedUpdatesMixin: |
786 | |
787 | @@ -553,7 +627,7 @@ |
788 | # If the bug is for a private project, then other direct subscribers |
789 | # should be unsubscribed. |
790 | |
791 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
792 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
793 | self.createBugTasksAndSubscribers()) |
794 | initial_subscribers = set(( |
795 | self.factory.makePerson(), bugtask_a.owner, bug_owner, |
796 | @@ -586,7 +660,7 @@ |
797 | # If the bug is for a private project, then other direct subscribers |
798 | # should be unsubscribed. |
799 | |
800 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
801 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
802 | self.createBugTasksAndSubscribers(private_security_related=True)) |
803 | initial_subscribers = set(( |
804 | self.factory.makePerson(), bug_owner, |
805 | @@ -617,10 +691,10 @@ |
806 | # If the bug is for a private project, then other direct subscribers |
807 | # should be unsubscribed. |
808 | |
809 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
810 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
811 | self.createBugTasksAndSubscribers(private_security_related=True)) |
812 | initial_subscribers = set(( |
813 | - self.factory.makePerson(), bug_owner, |
814 | + self.factory.makePerson(), bug_owner, |
815 | bugtask_a.pillar.security_contact, bugtask_a.pillar.driver, |
816 | bugtask_a.pillar.bug_supervisor)) |
817 | |
818 | @@ -644,7 +718,7 @@ |
819 | # When a bug is marked as private=false and security_related=false, |
820 | # any existing subscriptions are left alone. |
821 | |
822 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
823 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
824 | self.createBugTasksAndSubscribers(private_security_related=True)) |
825 | initial_subscribers = set(( |
826 | self.factory.makePerson(), bug_owner, |
827 | @@ -751,7 +825,7 @@ |
828 | # The bug supervisors are unsubscribed if a bug is made public and an |
829 | # email is sent telling them they have been unsubscribed. |
830 | |
831 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
832 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
833 | self.createBugTasksAndSubscribers(private_security_related=True)) |
834 | |
835 | with person_logged_in(bug_owner): |
836 | @@ -796,7 +870,7 @@ |
837 | # set to false and an email is sent telling them they have been |
838 | # unsubscribed. |
839 | |
840 | - (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
841 | + (bug, bug_owner, bugtask_a, bugtask_b, default_bugtask) = ( |
842 | self.createBugTasksAndSubscribers(private_security_related=True)) |
843 | |
844 | with person_logged_in(bug_owner): |
845 | |
846 | === modified file 'lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py' |
847 | --- lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-01-01 02:58:52 +0000 |
848 | +++ lib/lp/bugs/model/tests/test_bugsubscriptioninfo.py 2012-01-04 18:01:30 +0000 |
849 | @@ -46,7 +46,7 @@ |
850 | ] |
851 | observed = load_people( |
852 | Person.id.is_in(person.id for person in expected)) |
853 | - self.assertEqual(set(expected), set(observed)) |
854 | + self.assertContentEqual(expected, observed) |
855 | |
856 | |
857 | class TestSubscriptionRelatedSets(TestCaseWithFactory): |
858 | @@ -128,9 +128,8 @@ |
859 | with person_logged_in(self.bug.owner): |
860 | self.bug.unsubscribe(self.bug.owner, self.bug.owner) |
861 | |
862 | - def getInfo(self): |
863 | - return BugSubscriptionInfo( |
864 | - self.bug, BugNotificationLevel.LIFECYCLE) |
865 | + def getInfo(self, level=BugNotificationLevel.LIFECYCLE): |
866 | + return BugSubscriptionInfo(self.bug, level) |
867 | |
868 | def _create_direct_subscriptions(self): |
869 | subscribers = ( |
870 | @@ -142,22 +141,93 @@ |
871 | for subscriber in subscribers) |
872 | return subscribers, subscriptions |
873 | |
874 | + def test_forTask(self): |
875 | + # `forTask` returns a new `BugSubscriptionInfo` narrowed to the given |
876 | + # bugtask. |
877 | + info = self.getInfo() |
878 | + self.assertIs(None, info.bugtask) |
879 | + # If called with the current bugtask the same `BugSubscriptionInfo` |
880 | + # instance is returned. |
881 | + self.assertIs(info, info.forTask(info.bugtask)) |
882 | + # If called with a different bugtask a new `BugSubscriptionInfo` is |
883 | + # created. |
884 | + bugtask = self.bug.default_bugtask |
885 | + info_for_task = info.forTask(bugtask) |
886 | + self.assertIs(bugtask, info_for_task.bugtask) |
887 | + self.assertIsNot(info, info_for_task) |
888 | + # The instances share a cache of `BugSubscriptionInfo` instances. |
889 | + expected_cache = { |
890 | + info.cache_key: info, |
891 | + info_for_task.cache_key: info_for_task, |
892 | + } |
893 | + self.assertEqual(expected_cache, info.cache) |
894 | + self.assertIs(info.cache, info_for_task.cache) |
895 | + # Calling `forTask` again looks in the cache first. |
896 | + self.assertIs(info, info_for_task.forTask(info.bugtask)) |
897 | + self.assertIs(info_for_task, info.forTask(info_for_task.bugtask)) |
898 | + # The level is the same. |
899 | + self.assertEqual(info.level, info_for_task.level) |
900 | + |
901 | + def test_forLevel(self): |
902 | + # `forLevel` returns a new `BugSubscriptionInfo` narrowed to the given |
903 | + # subscription level. |
904 | + info = self.getInfo(BugNotificationLevel.LIFECYCLE) |
905 | + # If called with the current level the same `BugSubscriptionInfo` |
906 | + # instance is returned. |
907 | + self.assertIs(info, info.forLevel(info.level)) |
908 | + # If called with a different level a new `BugSubscriptionInfo` is |
909 | + # created. |
910 | + level = BugNotificationLevel.METADATA |
911 | + info_for_level = info.forLevel(level) |
912 | + self.assertEqual(level, info_for_level.level) |
913 | + self.assertIsNot(info, info_for_level) |
914 | + # The instances share a cache of `BugSubscriptionInfo` instances. |
915 | + expected_cache = { |
916 | + info.cache_key: info, |
917 | + info_for_level.cache_key: info_for_level, |
918 | + } |
919 | + self.assertEqual(expected_cache, info.cache) |
920 | + self.assertIs(info.cache, info_for_level.cache) |
921 | + # Calling `forLevel` again looks in the cache first. |
922 | + self.assertIs(info, info_for_level.forLevel(info.level)) |
923 | + self.assertIs(info_for_level, info.forLevel(info_for_level.level)) |
924 | + # The bugtask is the same. |
925 | + self.assertIs(info.bugtask, info_for_level.bugtask) |
926 | + |
927 | + def test_muted(self): |
928 | + # The set of muted subscribers for the bug. |
929 | + subscribers, subscriptions = self._create_direct_subscriptions() |
930 | + sub1, sub2 = subscribers |
931 | + with person_logged_in(sub1): |
932 | + self.bug.mute(sub1, sub1) |
933 | + self.assertContentEqual([sub1], self.getInfo().muted_subscribers) |
934 | + |
935 | def test_direct(self): |
936 | # The set of direct subscribers. |
937 | subscribers, subscriptions = self._create_direct_subscriptions() |
938 | found_subscriptions = self.getInfo().direct_subscriptions |
939 | - self.assertEqual(set(subscriptions), found_subscriptions) |
940 | - self.assertEqual(subscriptions, found_subscriptions.sorted) |
941 | - self.assertEqual(set(subscribers), found_subscriptions.subscribers) |
942 | - self.assertEqual(subscribers, found_subscriptions.subscribers.sorted) |
943 | + self.assertContentEqual(subscriptions, found_subscriptions) |
944 | + self.assertContentEqual(subscribers, found_subscriptions.subscribers) |
945 | |
946 | - def test_muted_direct(self): |
947 | + def test_direct_muted(self): |
948 | # If a direct is muted, it is not listed. |
949 | subscribers, subscriptions = self._create_direct_subscriptions() |
950 | with person_logged_in(subscribers[0]): |
951 | self.bug.mute(subscribers[0], subscribers[0]) |
952 | found_subscriptions = self.getInfo().direct_subscriptions |
953 | - self.assertEqual(set([subscriptions[1]]), found_subscriptions) |
954 | + self.assertContentEqual([subscriptions[1]], found_subscriptions) |
955 | + |
956 | + def test_all_direct(self): |
957 | + # The set of all direct subscribers, regardless of level. |
958 | + subscribers, subscriptions = self._create_direct_subscriptions() |
959 | + # Change the first subscription to be for comments only. |
960 | + sub1, sub2 = subscriptions |
961 | + with person_logged_in(sub1.person): |
962 | + sub1.bug_notification_level = BugNotificationLevel.LIFECYCLE |
963 | + info = self.getInfo(BugNotificationLevel.COMMENTS) |
964 | + self.assertContentEqual([sub2], info.direct_subscriptions) |
965 | + self.assertContentEqual( |
966 | + [sub1, sub2], info.direct_subscriptions_at_all_levels) |
967 | |
968 | def _create_duplicate_subscription(self): |
969 | duplicate_bug = self.factory.makeBug(product=self.target) |
970 | @@ -171,34 +241,26 @@ |
971 | def test_duplicate(self): |
972 | # The set of subscribers from duplicate bugs. |
973 | found_subscriptions = self.getInfo().duplicate_subscriptions |
974 | - self.assertEqual(set(), found_subscriptions) |
975 | - self.assertEqual((), found_subscriptions.sorted) |
976 | - self.assertEqual(set(), found_subscriptions.subscribers) |
977 | - self.assertEqual((), found_subscriptions.subscribers.sorted) |
978 | + self.assertContentEqual([], found_subscriptions) |
979 | + self.assertContentEqual([], found_subscriptions.subscribers) |
980 | duplicate_bug, duplicate_bug_subscription = ( |
981 | self._create_duplicate_subscription()) |
982 | found_subscriptions = self.getInfo().duplicate_subscriptions |
983 | - self.assertEqual( |
984 | - set([duplicate_bug_subscription]), |
985 | + self.assertContentEqual( |
986 | + [duplicate_bug_subscription], |
987 | found_subscriptions) |
988 | - self.assertEqual( |
989 | - (duplicate_bug_subscription,), |
990 | - found_subscriptions.sorted) |
991 | - self.assertEqual( |
992 | - set([duplicate_bug.owner]), |
993 | + self.assertContentEqual( |
994 | + [duplicate_bug.owner], |
995 | found_subscriptions.subscribers) |
996 | - self.assertEqual( |
997 | - (duplicate_bug.owner,), |
998 | - found_subscriptions.subscribers.sorted) |
999 | |
1000 | - def test_muted_duplicate(self): |
1001 | + def test_duplicate_muted(self): |
1002 | # If a duplicate is muted, it is not listed. |
1003 | duplicate_bug, duplicate_bug_subscription = ( |
1004 | self._create_duplicate_subscription()) |
1005 | with person_logged_in(duplicate_bug.owner): |
1006 | self.bug.mute(duplicate_bug.owner, duplicate_bug.owner) |
1007 | found_subscriptions = self.getInfo().duplicate_subscriptions |
1008 | - self.assertEqual(set(), found_subscriptions) |
1009 | + self.assertContentEqual([], found_subscriptions) |
1010 | |
1011 | def test_duplicate_for_private_bug(self): |
1012 | # The set of subscribers from duplicate bugs is always empty when the |
1013 | @@ -209,10 +271,8 @@ |
1014 | with person_logged_in(self.bug.owner): |
1015 | self.bug.setPrivate(True, self.bug.owner) |
1016 | found_subscriptions = self.getInfo().duplicate_subscriptions |
1017 | - self.assertEqual(set(), found_subscriptions) |
1018 | - self.assertEqual((), found_subscriptions.sorted) |
1019 | - self.assertEqual(set(), found_subscriptions.subscribers) |
1020 | - self.assertEqual((), found_subscriptions.subscribers.sorted) |
1021 | + self.assertContentEqual([], found_subscriptions) |
1022 | + self.assertContentEqual([], found_subscriptions.subscribers) |
1023 | |
1024 | def test_duplicate_only(self): |
1025 | # The set of duplicate subscriptions where the subscriber has no other |
1026 | @@ -224,8 +284,8 @@ |
1027 | duplicate_bug.getSubscriptionForPerson( |
1028 | duplicate_bug.owner)) |
1029 | found_subscriptions = self.getInfo().duplicate_only_subscriptions |
1030 | - self.assertEqual( |
1031 | - set([duplicate_bug_subscription]), |
1032 | + self.assertContentEqual( |
1033 | + [duplicate_bug_subscription], |
1034 | found_subscriptions) |
1035 | # If a user is subscribed to a duplicate bug and is a bugtask |
1036 | # assignee, for example, their duplicate subscription will not be |
1037 | @@ -234,71 +294,136 @@ |
1038 | self.bug.default_bugtask.transitionToAssignee( |
1039 | duplicate_bug_subscription.person) |
1040 | found_subscriptions = self.getInfo().duplicate_only_subscriptions |
1041 | - self.assertEqual(set(), found_subscriptions) |
1042 | - |
1043 | - def test_structural(self): |
1044 | + self.assertContentEqual([], found_subscriptions) |
1045 | + |
1046 | + def test_structural_subscriptions(self): |
1047 | + # The set of structural subscriptions. |
1048 | + subscribers = ( |
1049 | + self.factory.makePerson(), |
1050 | + self.factory.makePerson()) |
1051 | + with person_logged_in(self.bug.owner): |
1052 | + subscriptions = tuple( |
1053 | + self.target.addBugSubscription(subscriber, subscriber) |
1054 | + for subscriber in subscribers) |
1055 | + found_subscriptions = self.getInfo().structural_subscriptions |
1056 | + self.assertContentEqual(subscriptions, found_subscriptions) |
1057 | + |
1058 | + def test_structural_subscriptions_muted(self): |
1059 | + # The set of structural subscriptions DOES NOT exclude muted |
1060 | + # subscriptions. |
1061 | + subscriber = self.factory.makePerson() |
1062 | + with person_logged_in(subscriber): |
1063 | + self.bug.mute(subscriber, subscriber) |
1064 | + with person_logged_in(self.bug.owner): |
1065 | + subscription = self.target.addBugSubscription( |
1066 | + subscriber, subscriber) |
1067 | + found_subscriptions = self.getInfo().structural_subscriptions |
1068 | + self.assertContentEqual([subscription], found_subscriptions) |
1069 | + |
1070 | + def test_structural_subscribers(self): |
1071 | # The set of structural subscribers. |
1072 | subscribers = ( |
1073 | self.factory.makePerson(), |
1074 | self.factory.makePerson()) |
1075 | with person_logged_in(self.bug.owner): |
1076 | - subscriptions = tuple( |
1077 | + for subscriber in subscribers: |
1078 | self.target.addBugSubscription(subscriber, subscriber) |
1079 | - for subscriber in subscribers) |
1080 | - found_subscriptions = self.getInfo().structural_subscriptions |
1081 | - self.assertEqual(set(subscriptions), found_subscriptions) |
1082 | - self.assertEqual(subscriptions, found_subscriptions.sorted) |
1083 | - self.assertEqual(set(subscribers), found_subscriptions.subscribers) |
1084 | - self.assertEqual(subscribers, found_subscriptions.subscribers.sorted) |
1085 | + found_subscribers = self.getInfo().structural_subscribers |
1086 | + self.assertContentEqual(subscribers, found_subscribers) |
1087 | + |
1088 | + def test_structural_subscribers_muted(self): |
1089 | + # The set of structural subscribers DOES NOT exclude muted |
1090 | + # subscribers. |
1091 | + subscriber = self.factory.makePerson() |
1092 | + with person_logged_in(subscriber): |
1093 | + self.bug.mute(subscriber, subscriber) |
1094 | + with person_logged_in(self.bug.owner): |
1095 | + self.target.addBugSubscription(subscriber, subscriber) |
1096 | + found_subscribers = self.getInfo().structural_subscribers |
1097 | + self.assertContentEqual([subscriber], found_subscribers) |
1098 | |
1099 | def test_all_assignees(self): |
1100 | # The set of bugtask assignees for bugtasks that have been assigned. |
1101 | found_assignees = self.getInfo().all_assignees |
1102 | - self.assertEqual(set(), found_assignees) |
1103 | - self.assertEqual((), found_assignees.sorted) |
1104 | + self.assertContentEqual([], found_assignees) |
1105 | bugtask = self.bug.default_bugtask |
1106 | with person_logged_in(bugtask.pillar.bug_supervisor): |
1107 | bugtask.transitionToAssignee(self.bug.owner) |
1108 | found_assignees = self.getInfo().all_assignees |
1109 | - self.assertEqual(set([self.bug.owner]), found_assignees) |
1110 | - self.assertEqual((self.bug.owner,), found_assignees.sorted) |
1111 | + self.assertContentEqual([self.bug.owner], found_assignees) |
1112 | bugtask2 = self.factory.makeBugTask(bug=self.bug) |
1113 | with person_logged_in(bugtask2.pillar.owner): |
1114 | bugtask2.transitionToAssignee(bugtask2.owner) |
1115 | found_assignees = self.getInfo().all_assignees |
1116 | - self.assertEqual( |
1117 | - set([self.bug.owner, bugtask2.owner]), |
1118 | + self.assertContentEqual( |
1119 | + [self.bug.owner, bugtask2.owner], |
1120 | found_assignees) |
1121 | - self.assertEqual( |
1122 | - (self.bug.owner, bugtask2.owner), |
1123 | - found_assignees.sorted) |
1124 | + # Getting info for a specific bugtask will return the assignee for |
1125 | + # that bugtask only. |
1126 | + self.assertContentEqual( |
1127 | + [bugtask2.owner], |
1128 | + self.getInfo().forTask(bugtask2).all_assignees) |
1129 | |
1130 | def test_all_pillar_owners_without_bug_supervisors(self): |
1131 | # The set of owners of pillars for which no bug supervisor is |
1132 | - # configured. |
1133 | + # configured and which use Launchpad for bug tracking. |
1134 | [bugtask] = self.bug.bugtasks |
1135 | found_owners = ( |
1136 | self.getInfo().all_pillar_owners_without_bug_supervisors) |
1137 | - self.assertEqual(set(), found_owners) |
1138 | - self.assertEqual((), found_owners.sorted) |
1139 | - # Clear the supervisor for the first bugtask's target. |
1140 | + self.assertContentEqual([], found_owners) |
1141 | + # Clear the supervisor for the bugtask's target and ensure that the |
1142 | + # project uses Launchpad Bugs. |
1143 | with person_logged_in(bugtask.target.owner): |
1144 | bugtask.target.setBugSupervisor(None, bugtask.owner) |
1145 | + bugtask.pillar.official_malone = True |
1146 | + # The collection includes the pillar's owner. |
1147 | found_owners = ( |
1148 | self.getInfo().all_pillar_owners_without_bug_supervisors) |
1149 | - self.assertEqual(set([bugtask.pillar.owner]), found_owners) |
1150 | - self.assertEqual((bugtask.pillar.owner,), found_owners.sorted) |
1151 | - # Add another bugtask with a bug supervisor. |
1152 | - target2 = self.factory.makeProduct(bug_supervisor=None) |
1153 | + self.assertContentEqual([bugtask.pillar.owner], found_owners) |
1154 | + # Add another bugtask for a pillar that uses Launchpad but does not |
1155 | + # have a bug supervisor. |
1156 | + target2 = self.factory.makeProduct( |
1157 | + bug_supervisor=None, official_malone=True) |
1158 | bugtask2 = self.factory.makeBugTask(bug=self.bug, target=target2) |
1159 | found_owners = ( |
1160 | self.getInfo().all_pillar_owners_without_bug_supervisors) |
1161 | - self.assertEqual( |
1162 | - set([bugtask.pillar.owner, bugtask2.pillar.owner]), |
1163 | + self.assertContentEqual( |
1164 | + [bugtask.pillar.owner, bugtask2.pillar.owner], |
1165 | found_owners) |
1166 | - self.assertEqual( |
1167 | - (bugtask.pillar.owner, bugtask2.pillar.owner), |
1168 | - found_owners.sorted) |
1169 | + |
1170 | + def test_all_pillar_owners_without_bug_supervisors_not_using_malone(self): |
1171 | + # The set of owners of pillars for which no bug supervisor is |
1172 | + # configured and which do not use Launchpad for bug tracking is empty. |
1173 | + [bugtask] = self.bug.bugtasks |
1174 | + # Clear the supervisor for the first bugtask's target and ensure the |
1175 | + # project does not use Launchpad Bugs. |
1176 | + with person_logged_in(bugtask.target.owner): |
1177 | + bugtask.target.setBugSupervisor(None, bugtask.owner) |
1178 | + bugtask.pillar.official_malone = False |
1179 | + found_owners = ( |
1180 | + self.getInfo().all_pillar_owners_without_bug_supervisors) |
1181 | + self.assertContentEqual([], found_owners) |
1182 | + |
1183 | + def test_all_pillar_owners_without_bug_supervisors_for_bugtask(self): |
1184 | + # The set of the owner of the chosen bugtask's pillar when no bug |
1185 | + # supervisor is configured and which uses Launchpad for bug tracking. |
1186 | + [bugtask] = self.bug.bugtasks |
1187 | + # Clear the supervisor for the bugtask's target and ensure that the |
1188 | + # project uses Launchpad Bugs. |
1189 | + with person_logged_in(bugtask.target.owner): |
1190 | + bugtask.target.setBugSupervisor(None, bugtask.owner) |
1191 | + bugtask.pillar.official_malone = True |
1192 | + # Add another bugtask for a pillar that uses Launchpad but does not |
1193 | + # have a bug supervisor. |
1194 | + target2 = self.factory.makeProduct( |
1195 | + bug_supervisor=None, official_malone=True) |
1196 | + bugtask2 = self.factory.makeBugTask(bug=self.bug, target=target2) |
1197 | + # Getting subscription info for just a specific bugtask will yield |
1198 | + # owners for only the pillar associated with that bugtask. |
1199 | + info_for_bugtask2 = self.getInfo().forTask(bugtask2) |
1200 | + self.assertContentEqual( |
1201 | + [bugtask2.pillar.owner], |
1202 | + info_for_bugtask2.all_pillar_owners_without_bug_supervisors) |
1203 | |
1204 | def _create_also_notified_subscribers(self): |
1205 | # Add an assignee, a bug supervisor and a structural subscriber. |
1206 | @@ -318,8 +443,7 @@ |
1207 | def test_also_notified_subscribers(self): |
1208 | # The set of also notified subscribers. |
1209 | found_subscribers = self.getInfo().also_notified_subscribers |
1210 | - self.assertEqual(set(), found_subscribers) |
1211 | - self.assertEqual((), found_subscribers.sorted) |
1212 | + self.assertContentEqual([], found_subscribers) |
1213 | assignee, supervisor, structural_subscriber = ( |
1214 | self._create_also_notified_subscribers()) |
1215 | # Add a direct subscription. |
1216 | @@ -329,14 +453,11 @@ |
1217 | # The direct subscriber does not appear in the also notified set, but |
1218 | # the assignee, supervisor and structural subscriber do. |
1219 | found_subscribers = self.getInfo().also_notified_subscribers |
1220 | - self.assertEqual( |
1221 | - set([assignee, supervisor, structural_subscriber]), |
1222 | + self.assertContentEqual( |
1223 | + [assignee, supervisor, structural_subscriber], |
1224 | found_subscribers) |
1225 | - self.assertEqual( |
1226 | - (assignee, supervisor, structural_subscriber), |
1227 | - found_subscribers.sorted) |
1228 | |
1229 | - def test_muted_also_notified_subscribers(self): |
1230 | + def test_also_notified_subscribers_muted(self): |
1231 | # If someone is muted, they are not listed in the |
1232 | # also_notified_subscribers. |
1233 | assignee, supervisor, structural_subscriber = ( |
1234 | @@ -345,8 +466,8 @@ |
1235 | # the assignee, supervisor and structural subscriber do show up |
1236 | # when they are not muted. |
1237 | found_subscribers = self.getInfo().also_notified_subscribers |
1238 | - self.assertEqual( |
1239 | - set([assignee, supervisor, structural_subscriber]), |
1240 | + self.assertContentEqual( |
1241 | + [assignee, supervisor, structural_subscriber], |
1242 | found_subscribers) |
1243 | # Now we mute all of the subscribers. |
1244 | with person_logged_in(assignee): |
1245 | @@ -357,8 +478,7 @@ |
1246 | self.bug.mute(structural_subscriber, structural_subscriber) |
1247 | # Now we don't see them. |
1248 | found_subscribers = self.getInfo().also_notified_subscribers |
1249 | - self.assertEqual( |
1250 | - set(), found_subscribers) |
1251 | + self.assertContentEqual([], found_subscribers) |
1252 | |
1253 | def test_also_notified_subscribers_for_private_bug(self): |
1254 | # The set of also notified subscribers is always empty when the master |
1255 | @@ -370,8 +490,7 @@ |
1256 | with person_logged_in(self.bug.owner): |
1257 | self.bug.setPrivate(True, self.bug.owner) |
1258 | found_subscribers = self.getInfo().also_notified_subscribers |
1259 | - self.assertEqual(set(), found_subscribers) |
1260 | - self.assertEqual((), found_subscribers.sorted) |
1261 | + self.assertContentEqual([], found_subscribers) |
1262 | |
1263 | def test_indirect_subscribers(self): |
1264 | # The set of indirect subscribers is the union of also notified |
1265 | @@ -384,12 +503,9 @@ |
1266 | with person_logged_in(duplicate_bug.owner): |
1267 | duplicate_bug.markAsDuplicate(self.bug) |
1268 | found_subscribers = self.getInfo().indirect_subscribers |
1269 | - self.assertEqual( |
1270 | - set([assignee, duplicate_bug.owner]), |
1271 | + self.assertContentEqual( |
1272 | + [assignee, duplicate_bug.owner], |
1273 | found_subscribers) |
1274 | - self.assertEqual( |
1275 | - (assignee, duplicate_bug.owner), |
1276 | - found_subscribers.sorted) |
1277 | |
1278 | |
1279 | class TestBugSubscriptionInfoPermissions(TestCaseWithFactory): |
1280 | @@ -406,7 +522,7 @@ |
1281 | |
1282 | # All attributes require launchpad.View. |
1283 | permissions = set(checker.get_permissions.itervalues()) |
1284 | - self.assertEqual(set(["launchpad.View"]), permissions) |
1285 | + self.assertContentEqual(["launchpad.View"], permissions) |
1286 | |
1287 | # The security adapter for launchpad.View lets anyone reference the |
1288 | # attributes unless the bug is private, in which case only explicit |
1289 | @@ -444,27 +560,46 @@ |
1290 | yield recorder |
1291 | self.assertThat(recorder, condition) |
1292 | |
1293 | - def exercise_subscription_set(self, set_name): |
1294 | + def exercise_subscription_set(self, set_name, counts=(1, 1, 0)): |
1295 | + """Test the number of queries it takes to inspect a subscription set. |
1296 | + |
1297 | + :param set_name: The name of the set, e.g. "direct_subscriptions". |
1298 | + :param counts: A triple of the expected query counts for each of three |
1299 | + operations: get the set, get the set's subscribers, get the set's |
1300 | + subscribers in order. |
1301 | + """ |
1302 | # Looking up subscriptions takes a single query. |
1303 | - with self.exactly_x_queries(1): |
1304 | + with self.exactly_x_queries(counts[0]): |
1305 | getattr(self.info, set_name) |
1306 | # Getting the subscribers results in one additional query. |
1307 | - with self.exactly_x_queries(1): |
1308 | + with self.exactly_x_queries(counts[1]): |
1309 | getattr(self.info, set_name).subscribers |
1310 | # Everything is now cached so no more queries are needed. |
1311 | - with self.exactly_x_queries(0): |
1312 | + with self.exactly_x_queries(counts[2]): |
1313 | getattr(self.info, set_name).subscribers |
1314 | getattr(self.info, set_name).subscribers.sorted |
1315 | |
1316 | - def exercise_subscription_set_sorted_first(self, set_name): |
1317 | + def exercise_subscription_set_sorted_first( |
1318 | + self, set_name, counts=(1, 1, 0)): |
1319 | + """Test the number of queries it takes to inspect a subscription set. |
1320 | + |
1321 | + This differs from `exercise_subscription_set` in its second step, when |
1322 | + it looks at the sorted subscription list instead of the subscriber |
1323 | + set. |
1324 | + |
1325 | + :param set_name: The name of the set, e.g. "direct_subscriptions". |
1326 | + :param counts: A triple of the expected query counts for each of three |
1327 | + operations: get the set, get the set in order, get the set's |
1328 | + subscribers in order. |
1329 | + """ |
1330 | # Looking up subscriptions takes a single query. |
1331 | - with self.exactly_x_queries(1): |
1332 | + with self.exactly_x_queries(counts[0]): |
1333 | getattr(self.info, set_name) |
1334 | # Getting the sorted subscriptions takes one additional query. |
1335 | - with self.exactly_x_queries(1): |
1336 | + with self.exactly_x_queries(counts[1]): |
1337 | getattr(self.info, set_name).sorted |
1338 | # Everything is now cached so no more queries are needed. |
1339 | - with self.exactly_x_queries(0): |
1340 | + with self.exactly_x_queries(counts[2]): |
1341 | getattr(self.info, set_name).subscribers |
1342 | getattr(self.info, set_name).subscribers.sorted |
1343 | |
1344 | @@ -476,6 +611,10 @@ |
1345 | self.exercise_subscription_set_sorted_first( |
1346 | "direct_subscriptions") |
1347 | |
1348 | + def test_direct_subscriptions_at_all_levels(self): |
1349 | + self.exercise_subscription_set( |
1350 | + "direct_subscriptions_at_all_levels") |
1351 | + |
1352 | def make_duplicate_bug(self): |
1353 | duplicate_bug = self.factory.makeBug(product=self.target) |
1354 | with person_logged_in(duplicate_bug.owner): |
1355 | @@ -501,18 +640,19 @@ |
1356 | self.info.duplicate_subscriptions.subscribers |
1357 | |
1358 | def add_structural_subscriber(self): |
1359 | - with person_logged_in(self.bug.owner): |
1360 | - self.target.addSubscription(self.bug.owner, self.bug.owner) |
1361 | + subscriber = self.factory.makePerson() |
1362 | + with person_logged_in(subscriber): |
1363 | + self.target.addSubscription(subscriber, subscriber) |
1364 | |
1365 | def test_structural_subscriptions(self): |
1366 | self.add_structural_subscriber() |
1367 | self.exercise_subscription_set( |
1368 | - "structural_subscriptions") |
1369 | + "structural_subscriptions", (2, 1, 0)) |
1370 | |
1371 | def test_structural_subscriptions_sorted_first(self): |
1372 | self.add_structural_subscriber() |
1373 | self.exercise_subscription_set_sorted_first( |
1374 | - "structural_subscriptions") |
1375 | + "structural_subscriptions", (2, 1, 0)) |
1376 | |
1377 | def test_all_assignees(self): |
1378 | with self.exactly_x_queries(1): |
1379 | @@ -522,8 +662,8 @@ |
1380 | # Getting all bug supervisors and pillar owners can take several |
1381 | # queries. However, there are typically few tasks so the trade for |
1382 | # simplicity of implementation is acceptable. Only the simplest case |
1383 | - # is tested here. |
1384 | - with self.exactly_x_queries(1): |
1385 | + # is tested here (everything is already cached). |
1386 | + with self.exactly_x_queries(0): |
1387 | self.info.all_pillar_owners_without_bug_supervisors |
1388 | |
1389 | def test_also_notified_subscribers(self): |
1390 | @@ -536,7 +676,7 @@ |
1391 | self.info.all_assignees |
1392 | self.info.all_pillar_owners_without_bug_supervisors |
1393 | self.info.direct_subscriptions.subscribers |
1394 | - self.info.structural_subscriptions.subscribers |
1395 | + self.info.structural_subscribers |
1396 | with self.exactly_x_queries(1): |
1397 | self.info.also_notified_subscribers |
1398 | |
1399 | |
1400 | === modified file 'lib/lp/bugs/tests/test_structuralsubscription.py' |
1401 | --- lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-01 02:58:52 +0000 |
1402 | +++ lib/lp/bugs/tests/test_structuralsubscription.py 2012-01-04 18:01:30 +0000 |
1403 | @@ -27,8 +27,10 @@ |
1404 | from lp.bugs.model.structuralsubscription import ( |
1405 | get_structural_subscribers, |
1406 | get_structural_subscription_targets, |
1407 | + get_structural_subscriptions, |
1408 | get_structural_subscriptions_for_bug, |
1409 | ) |
1410 | +from lp.services.database.decoratedresultset import DecoratedResultSet |
1411 | from lp.testing import ( |
1412 | anonymous_logged_in, |
1413 | login_person, |
1414 | @@ -42,6 +44,9 @@ |
1415 | ) |
1416 | |
1417 | |
1418 | +RESULT_SETS = ResultSet, EmptyResultSet, DecoratedResultSet |
1419 | + |
1420 | + |
1421 | class TestStructuralSubscription(TestCaseWithFactory): |
1422 | |
1423 | layer = DatabaseFunctionalLayer |
1424 | @@ -634,6 +639,99 @@ |
1425 | self.assertEqual(set([self_sub]), set(subscriptions)) |
1426 | |
1427 | |
1428 | +class TestGetStructuralSubscriptions(TestCaseWithFactory): |
1429 | + |
1430 | + layer = DatabaseFunctionalLayer |
1431 | + |
1432 | + def make_product_with_bug(self): |
1433 | + product = self.factory.makeProduct() |
1434 | + bug = self.factory.makeBug(product=product) |
1435 | + return product, bug |
1436 | + |
1437 | + def test_get_structural_subscriptions_no_subscriptions(self): |
1438 | + # If there are no subscriptions for any of the bug's targets then no |
1439 | + # subscriptions will be returned by get_structural_subscriptions(). |
1440 | + product, bug = self.make_product_with_bug() |
1441 | + subscriptions = get_structural_subscriptions(bug, None) |
1442 | + self.assertIsInstance(subscriptions, RESULT_SETS) |
1443 | + self.assertEqual([], list(subscriptions)) |
1444 | + |
1445 | + def test_get_structural_subscriptions_single_target(self): |
1446 | + # Subscriptions for any of the bug's targets are returned. |
1447 | + subscriber = self.factory.makePerson() |
1448 | + login_person(subscriber) |
1449 | + product, bug = self.make_product_with_bug() |
1450 | + subscription = product.addBugSubscription(subscriber, subscriber) |
1451 | + self.assertContentEqual( |
1452 | + [subscription], get_structural_subscriptions(bug, None)) |
1453 | + |
1454 | + def test_get_structural_subscriptions_multiple_targets(self): |
1455 | + # Subscriptions for any of the bug's targets are returned. |
1456 | + actor = self.factory.makePerson() |
1457 | + login_person(actor) |
1458 | + |
1459 | + subscriber1 = self.factory.makePerson() |
1460 | + subscriber2 = self.factory.makePerson() |
1461 | + |
1462 | + product1 = self.factory.makeProduct(owner=actor) |
1463 | + subscription1 = product1.addBugSubscription(subscriber1, subscriber1) |
1464 | + product2 = self.factory.makeProduct(owner=actor) |
1465 | + subscription2 = product2.addBugSubscription(subscriber2, subscriber2) |
1466 | + |
1467 | + bug = self.factory.makeBug(product=product1) |
1468 | + bug.addTask(actor, product2) |
1469 | + |
1470 | + subscriptions = get_structural_subscriptions(bug, None) |
1471 | + self.assertIsInstance(subscriptions, RESULT_SETS) |
1472 | + self.assertContentEqual( |
1473 | + [subscription1, subscription2], subscriptions) |
1474 | + |
1475 | + def test_get_structural_subscriptions_multiple_targets_2(self): |
1476 | + # Only the first of multiple subscriptions for a person is returned |
1477 | + # when they have multiple matching subscriptions. |
1478 | + actor = self.factory.makePerson() |
1479 | + login_person(actor) |
1480 | + |
1481 | + subscriber = self.factory.makePerson() |
1482 | + product1 = self.factory.makeProduct(owner=actor) |
1483 | + subscription1 = product1.addBugSubscription(subscriber, subscriber) |
1484 | + product2 = self.factory.makeProduct(owner=actor) |
1485 | + product2.addBugSubscription(subscriber, subscriber) |
1486 | + |
1487 | + bug = self.factory.makeBug(product=product1) |
1488 | + bug.addTask(actor, product2) |
1489 | + |
1490 | + subscriptions = get_structural_subscriptions(bug, None) |
1491 | + self.assertIsInstance(subscriptions, RESULT_SETS) |
1492 | + self.assertContentEqual([subscription1], subscriptions) |
1493 | + |
1494 | + def test_get_structural_subscriptions_level(self): |
1495 | + # get_structural_subscriptions() respects the given level. |
1496 | + subscriber = self.factory.makePerson() |
1497 | + login_person(subscriber) |
1498 | + product, bug = self.make_product_with_bug() |
1499 | + subscription = product.addBugSubscription(subscriber, subscriber) |
1500 | + filter = subscription.bug_filters.one() |
1501 | + filter.bug_notification_level = BugNotificationLevel.METADATA |
1502 | + self.assertContentEqual( |
1503 | + [subscription], get_structural_subscriptions( |
1504 | + bug, BugNotificationLevel.METADATA)) |
1505 | + self.assertContentEqual( |
1506 | + [], get_structural_subscriptions( |
1507 | + bug, BugNotificationLevel.COMMENTS)) |
1508 | + |
1509 | + def test_get_structural_subscriptions_exclude(self): |
1510 | + # Subscriptions for any of the given excluded subscribers are not |
1511 | + # returned. |
1512 | + subscriber = self.factory.makePerson() |
1513 | + login_person(subscriber) |
1514 | + product, bug = self.make_product_with_bug() |
1515 | + product.addBugSubscription(subscriber, subscriber) |
1516 | + self.assertContentEqual( |
1517 | + [], get_structural_subscriptions( |
1518 | + bug, None, exclude=[subscriber])) |
1519 | + |
1520 | + |
1521 | class TestGetStructuralSubscribers(TestCaseWithFactory): |
1522 | |
1523 | layer = DatabaseFunctionalLayer |
1524 | @@ -648,7 +746,7 @@ |
1525 | # subscribers will be returned by get_structural_subscribers(). |
1526 | product, bug = self.make_product_with_bug() |
1527 | subscribers = get_structural_subscribers(bug, None, None, None) |
1528 | - self.assertIsInstance(subscribers, (ResultSet, EmptyResultSet)) |
1529 | + self.assertIsInstance(subscribers, RESULT_SETS) |
1530 | self.assertEqual([], list(subscribers)) |
1531 | |
1532 | def test_getStructuralSubscribers_single_target(self): |
1533 | @@ -678,7 +776,7 @@ |
1534 | bug.addTask(actor, product2) |
1535 | |
1536 | subscribers = get_structural_subscribers(bug, None, None, None) |
1537 | - self.assertIsInstance(subscribers, ResultSet) |
1538 | + self.assertIsInstance(subscribers, RESULT_SETS) |
1539 | self.assertEqual(set([subscriber1, subscriber2]), set(subscribers)) |
1540 | |
1541 | def test_getStructuralSubscribers_recipients(self): |
Fwiw, passes in devel:
Tests started at approximately 2012-01-03 16:07:06.335715 //bazaar. launchpad. net/~allenap/ launchpad/ bugnomination- timeout-bug-874250-preload-email r14523 //bazaar. launchpad. net/~launchpad- pqm/launchpad/ devel/ r14623
Source: bzr+ssh:
Target: bzr+ssh:
17326 tests run in 4:21:55.682084, 0 failures, 0 errors