Merge lp:~gary/launchpad/bug723999-2c into lp:launchpad

Proposed by Gary Poster
Status: Merged
Merged at revision: 12548
Proposed branch: lp:~gary/launchpad/bug723999-2c
Merge into: lp:launchpad
Prerequisite: lp:~gary/launchpad/bug723999-2b
Diff against target: 685 lines (+235/-218)
5 files modified
lib/lp/bugs/doc/bugsubscription.txt (+9/-2)
lib/lp/bugs/interfaces/bug.py (+5/-5)
lib/lp/bugs/model/bug.py (+68/-45)
lib/lp/bugs/model/structuralsubscription.py (+151/-120)
lib/lp/bugs/subscribers/bug.py (+2/-46)
To merge this branch: bzr merge lp:~gary/launchpad/bug723999-2c
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+52448@code.launchpad.net

Description of the change

This branch is slightly meatier than than the previous two in the series, and we're starting to see the light at the end of the tunnel.

This branch makes the following changes.

- I factored the code of bug.getAlsoNotifiedSubscribers into a function, get_also_notified_subscribers. The method now calls the function.
- I made the function accept a bugtask or a bug, rather than only a bug. This meant that I could eliminate an essentially identical function of lp.bugs.subscribers.bug.get_bugtask_indirect_subscribers and reuse the function. I changed the only test of the duplicate function to use the newly shared one.
- I made the get_also_notified_subscribers function proxied so that it returns proxied objects. This maintains the behavior of calling utility methods that are available to everyone, so it is a pattern I'm following in the cours eof this work. You'll see I actually verify the expected behavior of the proxy in some small test changes of the code formerly for get_bugtask_indirect_subscribers.
- I made the get_also_notified_subscribers function handle direct subscriptions more carefully.
- I made the get_also_notified_subscribers function use the relatively new structuralsubscription function get_structural_subscribers directly, rather than go through the middleman of the IBugTaskSet method. As you might guess, one of my upcoming branches will remove that IBugTaskSet method and repoint the pertinent tests.
- I made get_structural_subscribers and friends accept a direct_subscribers argument. If they have already been calculated, as they have in get_also_notified_subscribers, let's use them, and make our queries faster!
- I pulled out two functions from the _get_structural_subscription_filter_id_query function, _calculate_bugtask_condition and _calculate_tag_query. The only point of the refactoring at this time is to try to make the code more understandable by dividing up responsibilities a bit. This is my attempt to respond to jcsackett's review of my original bug723999 branch. If you don't find these changes more comprehensible, I've failed.

Note that I tried to make lint completely happy (particularly notable is that lint is what caused me to move the XXX comment in lib/lp/bugs/interfaces/bug.py), but one complaint left me at a loss.

./lib/lp/bugs/interfaces/bug.py
     435: E301 expected 1 blank line, found 0

Line 435 is the one below beginning with "@operation_parameters":

...
    def newMessage(owner, subject, content):
        """Create a new message, and link it to this object."""

    @operation_parameters(
        person=Reference(IPerson, title=_('Person'), required=True),
...

I don't see what the problem is. I've decided to ignore it.

Thank you!

Gary

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch is good to go. The descriptive and thorough introduction
helped quite a bit in understanding the changes.

As for the lint output, I determined that the comment embedded in the
decorator call is the culprit. I suggest filing a bug against
pocketlint and/or whichever Python linter it's using.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
2--- lib/lp/bugs/doc/bugsubscription.txt 2011-02-21 14:32:15 +0000
3+++ lib/lp/bugs/doc/bugsubscription.txt 2011-03-07 20:07:18 +0000
4@@ -106,10 +106,17 @@
5 It is also possible to get the list of indirect subscribers for an
6 individual bug task.
7
8- >>> from lp.bugs.subscribers.bug import get_bugtask_indirect_subscribers
9- >>> get_bugtask_indirect_subscribers(linux_source_bug.bugtasks[0])
10+ >>> from lp.bugs.model.bug import get_also_notified_subscribers
11+ >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
12+ >>> res
13 [<Person at ...>]
14
15+These are security proxied.
16+
17+ >>> from zope.security. proxy import Proxy
18+ >>> isinstance(res, Proxy)
19+ True
20+
21 The list of all bug subscribers can also be accessed via
22 IBugTask.bug_subscribers. Our event handling machinery compares a
23 "snapshot" of this value, before a bug was changed, to the current
24
25=== modified file 'lib/lp/bugs/interfaces/bug.py'
26--- lib/lp/bugs/interfaces/bug.py 2011-03-07 17:54:44 +0000
27+++ lib/lp/bugs/interfaces/bug.py 2011-03-07 20:07:18 +0000
28@@ -506,7 +506,7 @@
29 dupes, etc.
30 """
31
32- def getAlsoNotifiedSubscribers():
33+ def getAlsoNotifiedSubscribers(recipients=None, level=None):
34 """Return IPersons in the "Also notified" subscriber list.
35
36 This includes bug contacts and assignees, but not subscribers
37@@ -555,7 +555,7 @@
38
39 def addCommentNotification(message, recipients=None, activity=None):
40 """Add a bug comment notification.
41-
42+
43 If a BugActivity instance is provided as an `activity`, it is linked
44 to the notification."""
45
46@@ -1162,6 +1162,9 @@
47 """
48
49 def dangerousGetAllBugs():
50+ # XXX 2010-01-08 gmb bug=505850:
51+ # Note, this method should go away when we have a proper
52+ # permissions system for scripts.
53 """DO NOT CALL THIS METHOD.
54
55 This method exists solely to allow the bug heat script to grab
56@@ -1170,9 +1173,6 @@
57 DOING. AND IF YOU KNOW WHAT YOU'RE DOING YOU KNOW BETTER THAN TO
58 USE THIS ANYWAY.
59 """
60- # XXX 2010-01-08 gmb bug=505850:
61- # Note, this method should go away when we have a proper
62- # permissions system for scripts.
63
64 def getBugsWithOutdatedHeat(max_heat_age):
65 """Return the set of bugs whose heat is out of date.
66
67=== modified file 'lib/lp/bugs/model/bug.py'
68--- lib/lp/bugs/model/bug.py 2011-03-07 17:54:44 +0000
69+++ lib/lp/bugs/model/bug.py 2011-03-07 20:07:18 +0000
70@@ -14,6 +14,7 @@
71 'BugSet',
72 'BugTag',
73 'FileBugData',
74+ 'get_also_notified_subscribers',
75 'get_bug_tags',
76 'get_bug_tags_open_count',
77 ]
78@@ -70,7 +71,10 @@
79 implements,
80 providedBy,
81 )
82-from zope.security.proxy import removeSecurityProxy
83+from zope.security.proxy import (
84+ ProxyFactory,
85+ removeSecurityProxy,
86+ )
87
88 from canonical.config import config
89 from canonical.database.constants import UTC_NOW
90@@ -144,6 +148,7 @@
91 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
92 from lp.bugs.interfaces.bugtask import (
93 BugTaskStatus,
94+ IBugTask,
95 IBugTaskSet,
96 UNRESOLVED_BUGTASK_STATUSES,
97 )
98@@ -168,6 +173,7 @@
99 from lp.bugs.model.bugwatch import BugWatch
100 from lp.bugs.model.structuralsubscription import (
101 get_all_structural_subscriptions,
102+ get_structural_subscribers,
103 )
104 from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
105 from lp.registry.interfaces.distribution import IDistribution
106@@ -982,50 +988,7 @@
107 See the comment in getDirectSubscribers for a description of the
108 recipients argument.
109 """
110- if self.private:
111- return []
112-
113- also_notified_subscribers = set()
114-
115- for bugtask in self.bugtasks:
116- if bugtask.assignee:
117- also_notified_subscribers.add(bugtask.assignee)
118- if recipients is not None:
119- recipients.addAssignee(bugtask.assignee)
120-
121- # If the target's bug supervisor isn't set,
122- # we add the owner as a subscriber.
123- pillar = bugtask.pillar
124- if pillar.bug_supervisor is None and pillar.official_malone:
125- also_notified_subscribers.add(pillar.owner)
126- if recipients is not None:
127- recipients.addRegistrant(pillar.owner, pillar)
128-
129- # Structural subscribers.
130- if recipients is None:
131- temp_recipients = None
132- else:
133- temp_recipients = BugNotificationRecipients(
134- duplicateof=recipients.duplicateof)
135- also_notified_subscribers.update(
136- getUtility(IBugTaskSet).getStructuralSubscribers(
137- self.bugtasks, recipients=temp_recipients, level=level))
138-
139- # Direct subscriptions always take precedence over indirect
140- # subscriptions.
141- direct_subscribers = set(self.getDirectSubscribers())
142- if recipients is not None:
143- # A direct subscriber may have muted this notification.
144- # Therefore, we want to remove any direct subscribers from the
145- # structural subscription recipients before we merge.
146- temp_recipients.remove(direct_subscribers)
147- recipients.update(temp_recipients)
148-
149- # Remove security proxy for the sort key, but return
150- # the regular proxied object.
151- return sorted(
152- (also_notified_subscribers - direct_subscribers),
153- key=lambda x: removeSecurityProxy(x).displayname)
154+ return get_also_notified_subscribers(self, recipients, level)
155
156 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
157 level=None,
158@@ -2011,6 +1974,66 @@
159 operator.itemgetter(0))
160
161
162+@ProxyFactory
163+def get_also_notified_subscribers(
164+ bug_or_bugtask, recipients=None, level=None):
165+ """Return the indirect subscribers for a bug or bug task.
166+
167+ Return the list of people who should get notifications about changes
168+ to the bug or task because of having an indirect subscription
169+ relationship with it (by subscribing to a target, being an assignee
170+ or owner, etc...)
171+
172+ If `recipients` is present, add the subscribers to the set of
173+ bug notification recipients.
174+ """
175+ if IBug.providedBy(bug_or_bugtask):
176+ bug = bug_or_bugtask
177+ bugtasks = bug.bugtasks
178+ elif IBugTask.providedBy(bug_or_bugtask):
179+ bug = bug_or_bugtask.bug
180+ bugtasks = [bug_or_bugtask]
181+ else:
182+ raise ValueError('First argument must be bug or bugtask')
183+
184+ if bug.private:
185+ return []
186+
187+ # Direct subscriptions always take precedence over indirect
188+ # subscriptions.
189+ direct_subscribers = set(bug.getDirectSubscribers())
190+
191+ also_notified_subscribers = set()
192+
193+ for bugtask in bugtasks:
194+ if (bugtask.assignee and
195+ bugtask.assignee not in direct_subscribers):
196+ # We have an assignee that is not a direct subscriber.
197+ also_notified_subscribers.add(bugtask.assignee)
198+ if recipients is not None:
199+ recipients.addAssignee(bugtask.assignee)
200+
201+ # If the target's bug supervisor isn't set...
202+ pillar = bugtask.pillar
203+ if (pillar.bug_supervisor is None and
204+ pillar.official_malone and
205+ pillar.owner not in direct_subscribers):
206+ # ...we add the owner as a subscriber.
207+ also_notified_subscribers.add(pillar.owner)
208+ if recipients is not None:
209+ recipients.addRegistrant(pillar.owner, pillar)
210+
211+ # This structural subscribers code omits direct subscribers itself.
212+ also_notified_subscribers.update(
213+ get_structural_subscribers(
214+ bug_or_bugtask, recipients, level, direct_subscribers))
215+
216+ # Remove security proxy for the sort key, but return
217+ # the regular proxied object.
218+ return sorted(also_notified_subscribers,
219+ key=lambda x: removeSecurityProxy(x).displayname)
220+
221+
222 def load_people(*where):
223 """Get subscribers from subscriptions.
224
225
226=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
227--- lib/lp/bugs/model/structuralsubscription.py 2011-03-07 20:07:17 +0000
228+++ lib/lp/bugs/model/structuralsubscription.py 2011-03-07 20:07:18 +0000
229@@ -536,13 +536,16 @@
230 *conditions)
231
232
233-def get_structural_subscribers(bug_or_bugtask, recipients, level):
234+def get_structural_subscribers(
235+ bug_or_bugtask, recipients, level, direct_subscribers=None):
236 """Return subscribers for bug or bugtask at level.
237
238 :param bug: a bug.
239 :param recipients: a BugNotificationRecipients object or None.
240 Populates if given.
241 :param level: a level from lp.bugs.enum.BugNotificationLevel.
242+ :param direct_subscribers: a collection of Person objects who are
243+ directly subscribed to the bug.
244
245 Excludes structural subscriptions for people who are directly subscribed
246 to the bug."""
247@@ -559,7 +562,8 @@
248 # qastaging and production, _get_structural_subscription_filter_id_query
249 # can just return the query, and leave the candidates out of it.
250 candidates, filter_id_query = (
251- _get_structural_subscription_filter_id_query(bug, bugtasks, level))
252+ _get_structural_subscription_filter_id_query(
253+ bug, bugtasks, level, direct_subscribers))
254 if not candidates:
255 return EmptyResultSet()
256 # This is here because of a circular import.
257@@ -649,16 +653,17 @@
258 oper = "&&"
259
260
261-def _get_structural_subscription_filter_id_query(bug, bugtasks, level):
262+def _get_structural_subscription_filter_id_query(
263+ bug, bugtasks, level, direct_subscribers):
264 """Helper function.
265
266- This provides the core implementation for
267- get_structural_subscribers_for_bug and
268- get_structural_subscribers_for_bugtask.
269+ This provides the core implementation for get_structural_subscribers.
270
271 :param bug: a bug.
272 :param bugtasks: an iterable of one or more bugtasks of the bug.
273 :param level: a notification level.
274+ :param direct_subscribers: a collection of Person objects who are
275+ directly subscribed to the bug.
276 """
277 # We get the ids because we need to use group by in order to
278 # look at the filters' tags in aggregate. Once we have the ids,
279@@ -676,18 +681,26 @@
280 # See the docstring of get_structural_subscription_targets.
281 query_arguments = list(
282 get_structural_subscription_targets(bugtasks))
283- assert len(query_arguments) > 0, (
284- 'Programmer error: expected query arguments')
285+ if not query_arguments:
286+ # We have no bugtasks.
287+ return None, None
288 # With large numbers of filters in the system, it's fastest in our
289 # tests if we get a set of structural subscriptions pertinent to the
290 # given targets, and then work with that. It also comes in handy
291 # when we have to do a union, because we can share the work across
292 # the two queries.
293 # We will exclude people who have a direct subscription to the bug.
294- filters = [
295- Not(In(StructuralSubscription.subscriberID,
296- Select(BugSubscription.person_id,
297- BugSubscription.bug == bug)))]
298+ filters = []
299+ if direct_subscribers is not None:
300+ if direct_subscribers:
301+ filters.append(
302+ Not(In(StructuralSubscription.subscriberID,
303+ tuple(person.id for person in direct_subscribers))))
304+ else:
305+ filters.append(
306+ Not(In(StructuralSubscription.subscriberID,
307+ Select(BugSubscription.person_id,
308+ BugSubscription.bug == bug))))
309 candidates = _get_all_structural_subscriptions(
310 StructuralSubscription.id, query_arguments, *filters)
311 if not candidates:
312@@ -695,25 +708,21 @@
313 # then we don't need to look at the importance, status, and
314 # tags. We're done.
315 return None, None
316- # The "select_args" dictionary holds the arguments that we will
317- # pass to one or more SELECT clauses. We start with what will
318- # become the FROM clause. We always want the following Joins,
319- # so we can add them here at the beginning.
320- select_args = {
321- 'tables': [
322- StructuralSubscription,
323- Join(BugSubscriptionFilter,
324- BugSubscriptionFilter.structural_subscription_id ==
325- StructuralSubscription.id),
326- LeftJoin(BugSubscriptionFilterStatus,
327- BugSubscriptionFilterStatus.filter_id ==
328- BugSubscriptionFilter.id),
329- LeftJoin(BugSubscriptionFilterImportance,
330- BugSubscriptionFilterImportance.filter_id ==
331- BugSubscriptionFilter.id),
332- LeftJoin(BugSubscriptionFilterTag,
333- BugSubscriptionFilterTag.filter_id ==
334- BugSubscriptionFilter.id)]}
335+ # These are tables and joins we will want.
336+ tables = [
337+ StructuralSubscription,
338+ Join(BugSubscriptionFilter,
339+ BugSubscriptionFilter.structural_subscription_id ==
340+ StructuralSubscription.id),
341+ LeftJoin(BugSubscriptionFilterStatus,
342+ BugSubscriptionFilterStatus.filter_id ==
343+ BugSubscriptionFilter.id),
344+ LeftJoin(BugSubscriptionFilterImportance,
345+ BugSubscriptionFilterImportance.filter_id ==
346+ BugSubscriptionFilter.id),
347+ LeftJoin(BugSubscriptionFilterTag,
348+ BugSubscriptionFilterTag.filter_id ==
349+ BugSubscriptionFilter.id)]
350 # The "conditions" list will eventually be passed to a Storm
351 # "And" function, and then become the WHERE clause of our SELECT.
352 conditions = [In(StructuralSubscription.id, candidates)]
353@@ -721,14 +730,35 @@
354 if level is not None:
355 conditions.append(
356 BugSubscriptionFilter.bug_notification_level >= level)
357- # Now we handle importance and status, which are per bugtask.
358+ # This handles the bugtask-specific attributes of status and importance.
359+ conditions.append(_calculate_bugtask_condition(query_arguments))
360+ # Now we handle tags. This actually assembles the query, because it
361+ # may have to union two queries together.
362+ # Note that casting bug.tags to a list subtly removes the security
363+ # proxy on the list. Strings are never security-proxied, so we
364+ # don't have to worry about them.
365+ query = _calculate_tag_query(tables, conditions, list(bug.tags))
366+ # XXX gary 2011-03-03 bug 728818
367+ # Once we no longer have structural subscriptions without filters in
368+ # qastaging and production, _get_structural_subscription_filter_id_query
369+ # can just return the query, and leave the candidates out of it.
370+ return candidates, query
371+
372+
373+def _calculate_bugtask_condition(query_arguments):
374+ """Return a condition matching importance and status for the bugtasks.
375+
376+ :param query_arguments: an iterable of (bugtask, target) pairs, as
377+ returned by get_structural_subscription_targets.
378+ """
379+ # This handles importance and status, which are per bugtask.
380 # What we do is loop through the collection of bugtask, target
381 # in query_arguments. Each bugtask will have one or more
382 # targets that we have to check. We figure out how to describe each
383 # target using the useful IStructuralSubscriptionTargetHelper
384 # adapter, which has a "join" attribute on it that tells us how
385 # to distinguish that target. Once we have all of the target
386- # descriptins, we OR those together, and say that the filters
387+ # descriptions, we OR those together, and say that the filters
388 # for those targets must either have no importance or match the
389 # associated bugtask's importance; and have no status or match
390 # the bugtask's status. Once we have looked at all of the
391@@ -766,33 +796,40 @@
392 # We know there will be at least one bugtask, because we already
393 # escaped early "if len(query_arguments) == 0".
394 handle_bugtask_conditions(bugtask)
395- conditions.append(Or(*outer_or_conditions))
396- # Now we handle tags. If the bug has no tags, this is
397- # relatively easy. Otherwise, not so much.
398- tags = list(bug.tags) # This subtly removes the security proxy on
399- # the list. Strings are never security-proxied, so we don't have
400- # to worry about them.
401+ return Or(*outer_or_conditions)
402+
403+
404+def _calculate_tag_query(tables, conditions, tags):
405+ """Determine tag-related conditions and assemble a query.
406+
407+ :param tables: the tables and joins to use in the query.
408+ :param conditions: the other conditions that constrain the query.
409+ :param tags: the list of tags that the bug has.
410+ """
411+ # If the bug has no tags, this is relatively easy. Otherwise, not so
412+ # much.
413 if len(tags) == 0:
414 # The bug has no tags. We should leave out filters that
415 # require any generic non-empty set of tags
416 # (BugSubscriptionFilter.include_any_tags), which we do with
417- # the conditions. Then we can finish up the WHERE clause.
418- # Then we have to make sure that the filter does not require
419- # any *specific* tags. We do that with a GROUP BY on the
420- # filters, and then a HAVING clause that aggregates the
421- # BugSubscriptionFilterTags that are set to "include" the
422- # tag. (If it is not an include, that is an exclude, and a
423- # bug without tags will not have a particular tag, so we can
424- # ignore those in this case.) This requires a CASE
425- # statement within the COUNT. After this, we are done, and
426- # we return the fully formed SELECT query object.
427+ # the conditions.
428 conditions.append(Not(BugSubscriptionFilter.include_any_tags))
429- select_args['where'] = And(*conditions)
430- select_args['group_by'] = (BugSubscriptionFilter.id,)
431- select_args['having'] = Count(
432- SQL('CASE WHEN BugSubscriptionFilterTag.include '
433- 'THEN BugSubscriptionFilterTag.tag END'))==0
434- return candidates, Select(BugSubscriptionFilter.id, **select_args)
435+ return Select(
436+ BugSubscriptionFilter.id,
437+ tables=tables,
438+ where=And(*conditions),
439+ # We have to make sure that the filter does not require
440+ # any *specific* tags. We do that with a GROUP BY on the
441+ # filters, and then a HAVING clause that aggregates the
442+ # BugSubscriptionFilterTags that are set to "include" the
443+ # tag. (If it is not an include, that is an exclude, and a
444+ # bug without tags will not have a particular tag, so we can
445+ # ignore those in this case.) This requires a CASE
446+ # statement within the COUNT.
447+ group_by=(BugSubscriptionFilter.id,),
448+ having=Count(
449+ SQL('CASE WHEN BugSubscriptionFilterTag.include '
450+ 'THEN BugSubscriptionFilterTag.tag END'))==0)
451 else:
452 # The bug has some tags. This will require a bit of fancy
453 # footwork. First, though, we will simply want to leave out
454@@ -809,42 +846,32 @@
455 # involve another separate Postgres call, and I think that
456 # we've already gotten the big win by separating out the
457 # structural subscriptions into "candidates," above.
458- #
459- # So, up to now we've been assembling the things that are shared
460- # between the two queries, but now we start working on the
461- # differences between the two unioned queries. "first_select"
462- # will hold one set of arguments, and select_args will hold the
463- # other.
464- first_select = select_args.copy()
465+ # When Storm supports the WITH statement, we should reconsider
466+ # this (bug 729134).
467 # As mentioned, in this first SELECT we handle filters that
468 # match any of the filter's tags. This can be a relatively
469 # straightforward query--we just need a bit more added to
470 # our WHERE clause, and we don't need a GROUP BY/HAVING.
471- first_select['where'] = And(
472- Or(# We want filters that proclaim they simply want any tags.
473- BugSubscriptionFilter.include_any_tags,
474- # Also include filters that match any tag...
475- And(Not(BugSubscriptionFilter.find_all_tags),
476- Or(# ...with a positive match...
477- And(BugSubscriptionFilterTag.include,
478- In(BugSubscriptionFilterTag.tag, tags)),
479- # ...or with a negative match...
480- And(Not(BugSubscriptionFilterTag.include),
481- Not(In(BugSubscriptionFilterTag.tag, tags))),
482- # ...or if the filter does not specify any tags.
483- BugSubscriptionFilterTag.tag == None))),
484- *conditions)
485- first_select = Select(BugSubscriptionFilter.id, **first_select)
486+ first_select = Select(
487+ BugSubscriptionFilter.id,
488+ tables=tables,
489+ where=And(
490+ Or(# We want filters that proclaim they simply want any tags.
491+ BugSubscriptionFilter.include_any_tags,
492+ # Also include filters that match any tag...
493+ And(Not(BugSubscriptionFilter.find_all_tags),
494+ Or(# ...with a positive match...
495+ And(BugSubscriptionFilterTag.include,
496+ In(BugSubscriptionFilterTag.tag, tags)),
497+ # ...or with a negative match...
498+ And(Not(BugSubscriptionFilterTag.include),
499+ Not(In(BugSubscriptionFilterTag.tag, tags))),
500+ # ...or if the filter does not specify any tags.
501+ BugSubscriptionFilterTag.tag == None))),
502+ *conditions))
503 # We have our first clause. Now we start on the second one:
504- # handling filters that match *all* tags. Our WHERE clause
505- # is straightforward and, it should be clear that we are
506- # simply focusing on BugSubscriptionFilter.find_all_tags,
507- # when the first SELECT did not consider it.
508- select_args['where'] = And(
509- BugSubscriptionFilter.find_all_tags, *conditions)
510- # The GROUP BY collects the filters together.
511- select_args['group_by'] = (BugSubscriptionFilter.id,)
512- # Now it is time for the HAVING clause, which is where some
513+ # handling filters that match *all* tags.
514+ # This second query will have a HAVING clause, which is where some
515 # tricky bits happen. We first make a SQL snippet that
516 # represents the tags on this bug. It is straightforward
517 # except for one subtle hack: the addition of the empty
518@@ -868,39 +895,43 @@
519 # clearest alternative is defining a custom Postgres aggregator.
520 tags_array = "ARRAY[%s,' ']::TEXT[]" % ",".join(
521 quote(tag) for tag in tags)
522- # Now comes the HAVING clause itself.
523- select_args['having'] = And(
524- # The list of tags should be a superset of the filter tags to
525- # be included.
526- ArrayContains(
527- SQL(tags_array),
528- # This next line gives us an array of the tags that the
529- # filter wants to include. Notice that it includes the
530- # empty string when the condition does not match, per the
531- # discussion above.
532- ArrayAgg(
533- SQL("CASE WHEN BugSubscriptionFilterTag.include "
534- "THEN BugSubscriptionFilterTag.tag "
535- "ELSE ' '::TEXT END"))),
536- # The list of tags should also not intersect with the
537- # tags that the filter wants to exclude.
538- Not(
539- ArrayIntersects(
540+ # Now let's build the select itself.
541+ second_select = Select(
542+ BugSubscriptionFilter.id,
543+ tables=tables,
544+ # Our WHERE clause is straightforward. We are simply
545+ # focusing on BugSubscriptionFilter.find_all_tags, when the
546+ # first SELECT did not consider it.
547+ where=And(BugSubscriptionFilter.find_all_tags, *conditions),
548+ # The GROUP BY collects the filters together.
549+ group_by=(BugSubscriptionFilter.id,),
550+ having=And(
551+ # The list of tags should be a superset of the filter tags to
552+ # be included.
553+ ArrayContains(
554 SQL(tags_array),
555- # This next line gives us an array of the tags
556- # that the filter wants to exclude. We do not bother
557- # with the empty string, and therefore allow NULLs
558- # into the array, because in this case we are
559- # determining whether the sets intersect, not if the
560- # first set subsumes the second.
561+ # This next line gives us an array of the tags that the
562+ # filter wants to include. Notice that it includes the
563+ # empty string when the condition does not match, per the
564+ # discussion above.
565 ArrayAgg(
566- SQL('CASE WHEN '
567- 'NOT BugSubscriptionFilterTag.include '
568- 'THEN BugSubscriptionFilterTag.tag END')))))
569- # Everything is ready. Make our second SELECT statement, UNION
570- # it, and return it.
571- return candidates, Union(
572- first_select,
573- Select(
574- BugSubscriptionFilter.id,
575- **select_args))
576+ SQL("CASE WHEN BugSubscriptionFilterTag.include "
577+ "THEN BugSubscriptionFilterTag.tag "
578+ "ELSE ' '::TEXT END"))),
579+ # The list of tags should also not intersect with the
580+ # tags that the filter wants to exclude.
581+ Not(
582+ ArrayIntersects(
583+ SQL(tags_array),
584+ # This next line gives us an array of the tags
585+ # that the filter wants to exclude. We do not bother
586+ # with the empty string, and therefore allow NULLs
587+ # into the array, because in this case we are
588+ # determining whether the sets intersect, not if the
589+ # first set subsumes the second.
590+ ArrayAgg(
591+ SQL('CASE WHEN '
592+ 'NOT BugSubscriptionFilterTag.include '
593+ 'THEN BugSubscriptionFilterTag.tag END'))))))
594+ # Everything is ready. Return the union.
595+ return Union(first_select, second_select)
596
597=== modified file 'lib/lp/bugs/subscribers/bug.py'
598--- lib/lp/bugs/subscribers/bug.py 2011-02-11 22:04:25 +0000
599+++ lib/lp/bugs/subscribers/bug.py 2011-03-07 20:07:18 +0000
600@@ -5,7 +5,6 @@
601 __all__ = [
602 'add_bug_change_notifications',
603 'get_bug_delta',
604- 'get_bugtask_indirect_subscribers',
605 'notify_bug_attachment_added',
606 'notify_bug_attachment_removed',
607 'notify_bug_comment_added',
608@@ -16,9 +15,6 @@
609
610
611 import datetime
612-from operator import attrgetter
613-
614-from zope.component import getUtility
615
616 from canonical.config import config
617 from canonical.database.sqlbase import block_implicit_flushes
618@@ -35,10 +31,10 @@
619 )
620 from lp.bugs.adapters.bugdelta import BugDelta
621 from lp.bugs.enum import BugNotificationLevel
622-from lp.bugs.interfaces.bugtask import IBugTaskSet
623 from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
624 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
625 from lp.bugs.mail.newbug import generate_bug_add_email
626+from lp.bugs.model.bug import get_also_notified_subscribers
627 from lp.registry.interfaces.person import IPerson
628
629
630@@ -146,46 +142,6 @@
631 return None
632
633
634-def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
635- """Return the indirect subscribers for a bug task.
636-
637- Return the list of people who should get notifications about
638- changes to the task because of having an indirect subscription
639- relationship with it (by subscribing to its target, being an
640- assignee or owner, etc...)
641-
642- If `recipients` is present, add the subscribers to the set of
643- bug notification recipients.
644- """
645- if bugtask.bug.private:
646- return set()
647-
648- also_notified_subscribers = set()
649-
650- # Assignees are indirect subscribers.
651- if bugtask.assignee:
652- also_notified_subscribers.add(bugtask.assignee)
653- if recipients is not None:
654- recipients.addAssignee(bugtask.assignee)
655-
656- # Get structural subscribers.
657- also_notified_subscribers.update(
658- getUtility(IBugTaskSet).getStructuralSubscribers(
659- [bugtask], recipients, level))
660-
661- # If the target's bug supervisor isn't set,
662- # we add the owner as a subscriber.
663- pillar = bugtask.pillar
664- if pillar.bug_supervisor is None and pillar.official_malone:
665- also_notified_subscribers.add(pillar.owner)
666- if recipients is not None:
667- recipients.addRegistrant(pillar.owner, pillar)
668-
669- return sorted(
670- also_notified_subscribers,
671- key=attrgetter('displayname'))
672-
673-
674 def add_bug_change_notifications(bug_delta, old_bugtask=None,
675 new_subscribers=None):
676 """Generate bug notifications and add them to the bug."""
677@@ -195,7 +151,7 @@
678 level=BugNotificationLevel.METADATA)
679 if old_bugtask is not None:
680 old_bugtask_recipients = BugNotificationRecipients()
681- get_bugtask_indirect_subscribers(
682+ get_also_notified_subscribers(
683 old_bugtask, recipients=old_bugtask_recipients,
684 level=BugNotificationLevel.METADATA)
685 recipients.update(old_bugtask_recipients)