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
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2011-02-21 14:32:15 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2011-03-07 20:07:18 +0000
@@ -106,10 +106,17 @@
106It is also possible to get the list of indirect subscribers for an106It is also possible to get the list of indirect subscribers for an
107individual bug task.107individual bug task.
108108
109 >>> from lp.bugs.subscribers.bug import get_bugtask_indirect_subscribers109 >>> from lp.bugs.model.bug import get_also_notified_subscribers
110 >>> get_bugtask_indirect_subscribers(linux_source_bug.bugtasks[0])110 >>> res = get_also_notified_subscribers(linux_source_bug.bugtasks[0])
111 >>> res
111 [<Person at ...>]112 [<Person at ...>]
112113
114These are security proxied.
115
116 >>> from zope.security. proxy import Proxy
117 >>> isinstance(res, Proxy)
118 True
119
113The list of all bug subscribers can also be accessed via120The list of all bug subscribers can also be accessed via
114IBugTask.bug_subscribers. Our event handling machinery compares a121IBugTask.bug_subscribers. Our event handling machinery compares a
115"snapshot" of this value, before a bug was changed, to the current122"snapshot" of this value, before a bug was changed, to the current
116123
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2011-03-07 17:54:44 +0000
+++ lib/lp/bugs/interfaces/bug.py 2011-03-07 20:07:18 +0000
@@ -506,7 +506,7 @@
506 dupes, etc.506 dupes, etc.
507 """507 """
508508
509 def getAlsoNotifiedSubscribers():509 def getAlsoNotifiedSubscribers(recipients=None, level=None):
510 """Return IPersons in the "Also notified" subscriber list.510 """Return IPersons in the "Also notified" subscriber list.
511511
512 This includes bug contacts and assignees, but not subscribers512 This includes bug contacts and assignees, but not subscribers
@@ -555,7 +555,7 @@
555555
556 def addCommentNotification(message, recipients=None, activity=None):556 def addCommentNotification(message, recipients=None, activity=None):
557 """Add a bug comment notification.557 """Add a bug comment notification.
558 558
559 If a BugActivity instance is provided as an `activity`, it is linked559 If a BugActivity instance is provided as an `activity`, it is linked
560 to the notification."""560 to the notification."""
561561
@@ -1162,6 +1162,9 @@
1162 """1162 """
11631163
1164 def dangerousGetAllBugs():1164 def dangerousGetAllBugs():
1165 # XXX 2010-01-08 gmb bug=505850:
1166 # Note, this method should go away when we have a proper
1167 # permissions system for scripts.
1165 """DO NOT CALL THIS METHOD.1168 """DO NOT CALL THIS METHOD.
11661169
1167 This method exists solely to allow the bug heat script to grab1170 This method exists solely to allow the bug heat script to grab
@@ -1170,9 +1173,6 @@
1170 DOING. AND IF YOU KNOW WHAT YOU'RE DOING YOU KNOW BETTER THAN TO1173 DOING. AND IF YOU KNOW WHAT YOU'RE DOING YOU KNOW BETTER THAN TO
1171 USE THIS ANYWAY.1174 USE THIS ANYWAY.
1172 """1175 """
1173 # XXX 2010-01-08 gmb bug=505850:
1174 # Note, this method should go away when we have a proper
1175 # permissions system for scripts.
11761176
1177 def getBugsWithOutdatedHeat(max_heat_age):1177 def getBugsWithOutdatedHeat(max_heat_age):
1178 """Return the set of bugs whose heat is out of date.1178 """Return the set of bugs whose heat is out of date.
11791179
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-03-07 17:54:44 +0000
+++ lib/lp/bugs/model/bug.py 2011-03-07 20:07:18 +0000
@@ -14,6 +14,7 @@
14 'BugSet',14 'BugSet',
15 'BugTag',15 'BugTag',
16 'FileBugData',16 'FileBugData',
17 'get_also_notified_subscribers',
17 'get_bug_tags',18 'get_bug_tags',
18 'get_bug_tags_open_count',19 'get_bug_tags_open_count',
19 ]20 ]
@@ -70,7 +71,10 @@
70 implements,71 implements,
71 providedBy,72 providedBy,
72 )73 )
73from zope.security.proxy import removeSecurityProxy74from zope.security.proxy import (
75 ProxyFactory,
76 removeSecurityProxy,
77 )
7478
75from canonical.config import config79from canonical.config import config
76from canonical.database.constants import UTC_NOW80from canonical.database.constants import UTC_NOW
@@ -144,6 +148,7 @@
144from lp.bugs.interfaces.bugnotification import IBugNotificationSet148from lp.bugs.interfaces.bugnotification import IBugNotificationSet
145from lp.bugs.interfaces.bugtask import (149from lp.bugs.interfaces.bugtask import (
146 BugTaskStatus,150 BugTaskStatus,
151 IBugTask,
147 IBugTaskSet,152 IBugTaskSet,
148 UNRESOLVED_BUGTASK_STATUSES,153 UNRESOLVED_BUGTASK_STATUSES,
149 )154 )
@@ -168,6 +173,7 @@
168from lp.bugs.model.bugwatch import BugWatch173from lp.bugs.model.bugwatch import BugWatch
169from lp.bugs.model.structuralsubscription import (174from lp.bugs.model.structuralsubscription import (
170 get_all_structural_subscriptions,175 get_all_structural_subscriptions,
176 get_structural_subscribers,
171 )177 )
172from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet178from lp.hardwaredb.interfaces.hwdb import IHWSubmissionBugSet
173from lp.registry.interfaces.distribution import IDistribution179from lp.registry.interfaces.distribution import IDistribution
@@ -982,50 +988,7 @@
982 See the comment in getDirectSubscribers for a description of the988 See the comment in getDirectSubscribers for a description of the
983 recipients argument.989 recipients argument.
984 """990 """
985 if self.private:991 return get_also_notified_subscribers(self, recipients, level)
986 return []
987
988 also_notified_subscribers = set()
989
990 for bugtask in self.bugtasks:
991 if bugtask.assignee:
992 also_notified_subscribers.add(bugtask.assignee)
993 if recipients is not None:
994 recipients.addAssignee(bugtask.assignee)
995
996 # If the target's bug supervisor isn't set,
997 # we add the owner as a subscriber.
998 pillar = bugtask.pillar
999 if pillar.bug_supervisor is None and pillar.official_malone:
1000 also_notified_subscribers.add(pillar.owner)
1001 if recipients is not None:
1002 recipients.addRegistrant(pillar.owner, pillar)
1003
1004 # Structural subscribers.
1005 if recipients is None:
1006 temp_recipients = None
1007 else:
1008 temp_recipients = BugNotificationRecipients(
1009 duplicateof=recipients.duplicateof)
1010 also_notified_subscribers.update(
1011 getUtility(IBugTaskSet).getStructuralSubscribers(
1012 self.bugtasks, recipients=temp_recipients, level=level))
1013
1014 # Direct subscriptions always take precedence over indirect
1015 # subscriptions.
1016 direct_subscribers = set(self.getDirectSubscribers())
1017 if recipients is not None:
1018 # A direct subscriber may have muted this notification.
1019 # Therefore, we want to remove any direct subscribers from the
1020 # structural subscription recipients before we merge.
1021 temp_recipients.remove(direct_subscribers)
1022 recipients.update(temp_recipients)
1023
1024 # Remove security proxy for the sort key, but return
1025 # the regular proxied object.
1026 return sorted(
1027 (also_notified_subscribers - direct_subscribers),
1028 key=lambda x: removeSecurityProxy(x).displayname)
1029992
1030 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,993 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
1031 level=None,994 level=None,
@@ -2011,6 +1974,66 @@
2011 operator.itemgetter(0))1974 operator.itemgetter(0))
20121975
20131976
1977@ProxyFactory
1978def get_also_notified_subscribers(
1979 bug_or_bugtask, recipients=None, level=None):
1980 """Return the indirect subscribers for a bug or bug task.
1981
1982 Return the list of people who should get notifications about changes
1983 to the bug or task because of having an indirect subscription
1984 relationship with it (by subscribing to a target, being an assignee
1985 or owner, etc...)
1986
1987 If `recipients` is present, add the subscribers to the set of
1988 bug notification recipients.
1989 """
1990 if IBug.providedBy(bug_or_bugtask):
1991 bug = bug_or_bugtask
1992 bugtasks = bug.bugtasks
1993 elif IBugTask.providedBy(bug_or_bugtask):
1994 bug = bug_or_bugtask.bug
1995 bugtasks = [bug_or_bugtask]
1996 else:
1997 raise ValueError('First argument must be bug or bugtask')
1998
1999 if bug.private:
2000 return []
2001
2002 # Direct subscriptions always take precedence over indirect
2003 # subscriptions.
2004 direct_subscribers = set(bug.getDirectSubscribers())
2005
2006 also_notified_subscribers = set()
2007
2008 for bugtask in bugtasks:
2009 if (bugtask.assignee and
2010 bugtask.assignee not in direct_subscribers):
2011 # We have an assignee that is not a direct subscriber.
2012 also_notified_subscribers.add(bugtask.assignee)
2013 if recipients is not None:
2014 recipients.addAssignee(bugtask.assignee)
2015
2016 # If the target's bug supervisor isn't set...
2017 pillar = bugtask.pillar
2018 if (pillar.bug_supervisor is None and
2019 pillar.official_malone and
2020 pillar.owner not in direct_subscribers):
2021 # ...we add the owner as a subscriber.
2022 also_notified_subscribers.add(pillar.owner)
2023 if recipients is not None:
2024 recipients.addRegistrant(pillar.owner, pillar)
2025
2026 # This structural subscribers code omits direct subscribers itself.
2027 also_notified_subscribers.update(
2028 get_structural_subscribers(
2029 bug_or_bugtask, recipients, level, direct_subscribers))
2030
2031 # Remove security proxy for the sort key, but return
2032 # the regular proxied object.
2033 return sorted(also_notified_subscribers,
2034 key=lambda x: removeSecurityProxy(x).displayname)
2035
2036
2014def load_people(*where):2037def load_people(*where):
2015 """Get subscribers from subscriptions.2038 """Get subscribers from subscriptions.
20162039
20172040
=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
--- lib/lp/bugs/model/structuralsubscription.py 2011-03-07 20:07:17 +0000
+++ lib/lp/bugs/model/structuralsubscription.py 2011-03-07 20:07:18 +0000
@@ -536,13 +536,16 @@
536 *conditions)536 *conditions)
537537
538538
539def get_structural_subscribers(bug_or_bugtask, recipients, level):539def get_structural_subscribers(
540 bug_or_bugtask, recipients, level, direct_subscribers=None):
540 """Return subscribers for bug or bugtask at level.541 """Return subscribers for bug or bugtask at level.
541542
542 :param bug: a bug.543 :param bug: a bug.
543 :param recipients: a BugNotificationRecipients object or None.544 :param recipients: a BugNotificationRecipients object or None.
544 Populates if given.545 Populates if given.
545 :param level: a level from lp.bugs.enum.BugNotificationLevel.546 :param level: a level from lp.bugs.enum.BugNotificationLevel.
547 :param direct_subscribers: a collection of Person objects who are
548 directly subscribed to the bug.
546549
547 Excludes structural subscriptions for people who are directly subscribed550 Excludes structural subscriptions for people who are directly subscribed
548 to the bug."""551 to the bug."""
@@ -559,7 +562,8 @@
559 # qastaging and production, _get_structural_subscription_filter_id_query562 # qastaging and production, _get_structural_subscription_filter_id_query
560 # can just return the query, and leave the candidates out of it.563 # can just return the query, and leave the candidates out of it.
561 candidates, filter_id_query = (564 candidates, filter_id_query = (
562 _get_structural_subscription_filter_id_query(bug, bugtasks, level))565 _get_structural_subscription_filter_id_query(
566 bug, bugtasks, level, direct_subscribers))
563 if not candidates:567 if not candidates:
564 return EmptyResultSet()568 return EmptyResultSet()
565 # This is here because of a circular import.569 # This is here because of a circular import.
@@ -649,16 +653,17 @@
649 oper = "&&"653 oper = "&&"
650654
651655
652def _get_structural_subscription_filter_id_query(bug, bugtasks, level):656def _get_structural_subscription_filter_id_query(
657 bug, bugtasks, level, direct_subscribers):
653 """Helper function.658 """Helper function.
654659
655 This provides the core implementation for660 This provides the core implementation for get_structural_subscribers.
656 get_structural_subscribers_for_bug and
657 get_structural_subscribers_for_bugtask.
658661
659 :param bug: a bug.662 :param bug: a bug.
660 :param bugtasks: an iterable of one or more bugtasks of the bug.663 :param bugtasks: an iterable of one or more bugtasks of the bug.
661 :param level: a notification level.664 :param level: a notification level.
665 :param direct_subscribers: a collection of Person objects who are
666 directly subscribed to the bug.
662 """667 """
663 # We get the ids because we need to use group by in order to668 # We get the ids because we need to use group by in order to
664 # look at the filters' tags in aggregate. Once we have the ids,669 # look at the filters' tags in aggregate. Once we have the ids,
@@ -676,18 +681,26 @@
676 # See the docstring of get_structural_subscription_targets.681 # See the docstring of get_structural_subscription_targets.
677 query_arguments = list(682 query_arguments = list(
678 get_structural_subscription_targets(bugtasks))683 get_structural_subscription_targets(bugtasks))
679 assert len(query_arguments) > 0, (684 if not query_arguments:
680 'Programmer error: expected query arguments')685 # We have no bugtasks.
686 return None, None
681 # With large numbers of filters in the system, it's fastest in our687 # With large numbers of filters in the system, it's fastest in our
682 # tests if we get a set of structural subscriptions pertinent to the688 # tests if we get a set of structural subscriptions pertinent to the
683 # given targets, and then work with that. It also comes in handy689 # given targets, and then work with that. It also comes in handy
684 # when we have to do a union, because we can share the work across690 # when we have to do a union, because we can share the work across
685 # the two queries.691 # the two queries.
686 # We will exclude people who have a direct subscription to the bug.692 # We will exclude people who have a direct subscription to the bug.
687 filters = [693 filters = []
688 Not(In(StructuralSubscription.subscriberID,694 if direct_subscribers is not None:
689 Select(BugSubscription.person_id,695 if direct_subscribers:
690 BugSubscription.bug == bug)))]696 filters.append(
697 Not(In(StructuralSubscription.subscriberID,
698 tuple(person.id for person in direct_subscribers))))
699 else:
700 filters.append(
701 Not(In(StructuralSubscription.subscriberID,
702 Select(BugSubscription.person_id,
703 BugSubscription.bug == bug))))
691 candidates = _get_all_structural_subscriptions(704 candidates = _get_all_structural_subscriptions(
692 StructuralSubscription.id, query_arguments, *filters)705 StructuralSubscription.id, query_arguments, *filters)
693 if not candidates:706 if not candidates:
@@ -695,25 +708,21 @@
695 # then we don't need to look at the importance, status, and708 # then we don't need to look at the importance, status, and
696 # tags. We're done.709 # tags. We're done.
697 return None, None710 return None, None
698 # The "select_args" dictionary holds the arguments that we will711 # These are tables and joins we will want.
699 # pass to one or more SELECT clauses. We start with what will712 tables = [
700 # become the FROM clause. We always want the following Joins,713 StructuralSubscription,
701 # so we can add them here at the beginning.714 Join(BugSubscriptionFilter,
702 select_args = {715 BugSubscriptionFilter.structural_subscription_id ==
703 'tables': [716 StructuralSubscription.id),
704 StructuralSubscription,717 LeftJoin(BugSubscriptionFilterStatus,
705 Join(BugSubscriptionFilter,718 BugSubscriptionFilterStatus.filter_id ==
706 BugSubscriptionFilter.structural_subscription_id ==719 BugSubscriptionFilter.id),
707 StructuralSubscription.id),720 LeftJoin(BugSubscriptionFilterImportance,
708 LeftJoin(BugSubscriptionFilterStatus,721 BugSubscriptionFilterImportance.filter_id ==
709 BugSubscriptionFilterStatus.filter_id ==722 BugSubscriptionFilter.id),
710 BugSubscriptionFilter.id),723 LeftJoin(BugSubscriptionFilterTag,
711 LeftJoin(BugSubscriptionFilterImportance,724 BugSubscriptionFilterTag.filter_id ==
712 BugSubscriptionFilterImportance.filter_id ==725 BugSubscriptionFilter.id)]
713 BugSubscriptionFilter.id),
714 LeftJoin(BugSubscriptionFilterTag,
715 BugSubscriptionFilterTag.filter_id ==
716 BugSubscriptionFilter.id)]}
717 # The "conditions" list will eventually be passed to a Storm726 # The "conditions" list will eventually be passed to a Storm
718 # "And" function, and then become the WHERE clause of our SELECT.727 # "And" function, and then become the WHERE clause of our SELECT.
719 conditions = [In(StructuralSubscription.id, candidates)]728 conditions = [In(StructuralSubscription.id, candidates)]
@@ -721,14 +730,35 @@
721 if level is not None:730 if level is not None:
722 conditions.append(731 conditions.append(
723 BugSubscriptionFilter.bug_notification_level >= level)732 BugSubscriptionFilter.bug_notification_level >= level)
724 # Now we handle importance and status, which are per bugtask.733 # This handles the bugtask-specific attributes of status and importance.
734 conditions.append(_calculate_bugtask_condition(query_arguments))
735 # Now we handle tags. This actually assembles the query, because it
736 # may have to union two queries together.
737 # Note that casting bug.tags to a list subtly removes the security
738 # proxy on the list. Strings are never security-proxied, so we
739 # don't have to worry about them.
740 query = _calculate_tag_query(tables, conditions, list(bug.tags))
741 # XXX gary 2011-03-03 bug 728818
742 # Once we no longer have structural subscriptions without filters in
743 # qastaging and production, _get_structural_subscription_filter_id_query
744 # can just return the query, and leave the candidates out of it.
745 return candidates, query
746
747
748def _calculate_bugtask_condition(query_arguments):
749 """Return a condition matching importance and status for the bugtasks.
750
751 :param query_arguments: an iterable of (bugtask, target) pairs, as
752 returned by get_structural_subscription_targets.
753 """
754 # This handles importance and status, which are per bugtask.
725 # What we do is loop through the collection of bugtask, target755 # What we do is loop through the collection of bugtask, target
726 # in query_arguments. Each bugtask will have one or more756 # in query_arguments. Each bugtask will have one or more
727 # targets that we have to check. We figure out how to describe each757 # targets that we have to check. We figure out how to describe each
728 # target using the useful IStructuralSubscriptionTargetHelper758 # target using the useful IStructuralSubscriptionTargetHelper
729 # adapter, which has a "join" attribute on it that tells us how759 # adapter, which has a "join" attribute on it that tells us how
730 # to distinguish that target. Once we have all of the target760 # to distinguish that target. Once we have all of the target
731 # descriptins, we OR those together, and say that the filters761 # descriptions, we OR those together, and say that the filters
732 # for those targets must either have no importance or match the762 # for those targets must either have no importance or match the
733 # associated bugtask's importance; and have no status or match763 # associated bugtask's importance; and have no status or match
734 # the bugtask's status. Once we have looked at all of the764 # the bugtask's status. Once we have looked at all of the
@@ -766,33 +796,40 @@
766 # We know there will be at least one bugtask, because we already796 # We know there will be at least one bugtask, because we already
767 # escaped early "if len(query_arguments) == 0".797 # escaped early "if len(query_arguments) == 0".
768 handle_bugtask_conditions(bugtask)798 handle_bugtask_conditions(bugtask)
769 conditions.append(Or(*outer_or_conditions))799 return Or(*outer_or_conditions)
770 # Now we handle tags. If the bug has no tags, this is800
771 # relatively easy. Otherwise, not so much.801
772 tags = list(bug.tags) # This subtly removes the security proxy on802def _calculate_tag_query(tables, conditions, tags):
773 # the list. Strings are never security-proxied, so we don't have803 """Determine tag-related conditions and assemble a query.
774 # to worry about them.804
805 :param tables: the tables and joins to use in the query.
806 :param conditions: the other conditions that constrain the query.
807 :param tags: the list of tags that the bug has.
808 """
809 # If the bug has no tags, this is relatively easy. Otherwise, not so
810 # much.
775 if len(tags) == 0:811 if len(tags) == 0:
776 # The bug has no tags. We should leave out filters that812 # The bug has no tags. We should leave out filters that
777 # require any generic non-empty set of tags813 # require any generic non-empty set of tags
778 # (BugSubscriptionFilter.include_any_tags), which we do with814 # (BugSubscriptionFilter.include_any_tags), which we do with
779 # the conditions. Then we can finish up the WHERE clause.815 # the conditions.
780 # Then we have to make sure that the filter does not require
781 # any *specific* tags. We do that with a GROUP BY on the
782 # filters, and then a HAVING clause that aggregates the
783 # BugSubscriptionFilterTags that are set to "include" the
784 # tag. (If it is not an include, that is an exclude, and a
785 # bug without tags will not have a particular tag, so we can
786 # ignore those in this case.) This requires a CASE
787 # statement within the COUNT. After this, we are done, and
788 # we return the fully formed SELECT query object.
789 conditions.append(Not(BugSubscriptionFilter.include_any_tags))816 conditions.append(Not(BugSubscriptionFilter.include_any_tags))
790 select_args['where'] = And(*conditions)817 return Select(
791 select_args['group_by'] = (BugSubscriptionFilter.id,)818 BugSubscriptionFilter.id,
792 select_args['having'] = Count(819 tables=tables,
793 SQL('CASE WHEN BugSubscriptionFilterTag.include '820 where=And(*conditions),
794 'THEN BugSubscriptionFilterTag.tag END'))==0821 # We have to make sure that the filter does not require
795 return candidates, Select(BugSubscriptionFilter.id, **select_args)822 # any *specific* tags. We do that with a GROUP BY on the
823 # filters, and then a HAVING clause that aggregates the
824 # BugSubscriptionFilterTags that are set to "include" the
825 # tag. (If it is not an include, that is an exclude, and a
826 # bug without tags will not have a particular tag, so we can
827 # ignore those in this case.) This requires a CASE
828 # statement within the COUNT.
829 group_by=(BugSubscriptionFilter.id,),
830 having=Count(
831 SQL('CASE WHEN BugSubscriptionFilterTag.include '
832 'THEN BugSubscriptionFilterTag.tag END'))==0)
796 else:833 else:
797 # The bug has some tags. This will require a bit of fancy834 # The bug has some tags. This will require a bit of fancy
798 # footwork. First, though, we will simply want to leave out835 # footwork. First, though, we will simply want to leave out
@@ -809,42 +846,32 @@
809 # involve another separate Postgres call, and I think that846 # involve another separate Postgres call, and I think that
810 # we've already gotten the big win by separating out the847 # we've already gotten the big win by separating out the
811 # structural subscriptions into "candidates," above.848 # structural subscriptions into "candidates," above.
812 #849 # When Storm supports the WITH statement, we should reconsider
813 # So, up to now we've been assembling the things that are shared850 # this (bug 729134).
814 # between the two queries, but now we start working on the
815 # differences between the two unioned queries. "first_select"
816 # will hold one set of arguments, and select_args will hold the
817 # other.
818 first_select = select_args.copy()
819 # As mentioned, in this first SELECT we handle filters that851 # As mentioned, in this first SELECT we handle filters that
820 # match any of the filter's tags. This can be a relatively852 # match any of the filter's tags. This can be a relatively
821 # straightforward query--we just need a bit more added to853 # straightforward query--we just need a bit more added to
822 # our WHERE clause, and we don't need a GROUP BY/HAVING.854 # our WHERE clause, and we don't need a GROUP BY/HAVING.
823 first_select['where'] = And(855 first_select = Select(
824 Or(# We want filters that proclaim they simply want any tags.856 BugSubscriptionFilter.id,
825 BugSubscriptionFilter.include_any_tags,857 tables=tables,
826 # Also include filters that match any tag...858 where=And(
827 And(Not(BugSubscriptionFilter.find_all_tags),859 Or(# We want filters that proclaim they simply want any tags.
828 Or(# ...with a positive match...860 BugSubscriptionFilter.include_any_tags,
829 And(BugSubscriptionFilterTag.include,861 # Also include filters that match any tag...
830 In(BugSubscriptionFilterTag.tag, tags)),862 And(Not(BugSubscriptionFilter.find_all_tags),
831 # ...or with a negative match...863 Or(# ...with a positive match...
832 And(Not(BugSubscriptionFilterTag.include),864 And(BugSubscriptionFilterTag.include,
833 Not(In(BugSubscriptionFilterTag.tag, tags))),865 In(BugSubscriptionFilterTag.tag, tags)),
834 # ...or if the filter does not specify any tags.866 # ...or with a negative match...
835 BugSubscriptionFilterTag.tag == None))),867 And(Not(BugSubscriptionFilterTag.include),
836 *conditions)868 Not(In(BugSubscriptionFilterTag.tag, tags))),
837 first_select = Select(BugSubscriptionFilter.id, **first_select)869 # ...or if the filter does not specify any tags.
870 BugSubscriptionFilterTag.tag == None))),
871 *conditions))
838 # We have our first clause. Now we start on the second one:872 # We have our first clause. Now we start on the second one:
839 # handling filters that match *all* tags. Our WHERE clause873 # handling filters that match *all* tags.
840 # is straightforward and, it should be clear that we are874 # This second query will have a HAVING clause, which is where some
841 # simply focusing on BugSubscriptionFilter.find_all_tags,
842 # when the first SELECT did not consider it.
843 select_args['where'] = And(
844 BugSubscriptionFilter.find_all_tags, *conditions)
845 # The GROUP BY collects the filters together.
846 select_args['group_by'] = (BugSubscriptionFilter.id,)
847 # Now it is time for the HAVING clause, which is where some
848 # tricky bits happen. We first make a SQL snippet that875 # tricky bits happen. We first make a SQL snippet that
849 # represents the tags on this bug. It is straightforward876 # represents the tags on this bug. It is straightforward
850 # except for one subtle hack: the addition of the empty877 # except for one subtle hack: the addition of the empty
@@ -868,39 +895,43 @@
868 # clearest alternative is defining a custom Postgres aggregator.895 # clearest alternative is defining a custom Postgres aggregator.
869 tags_array = "ARRAY[%s,' ']::TEXT[]" % ",".join(896 tags_array = "ARRAY[%s,' ']::TEXT[]" % ",".join(
870 quote(tag) for tag in tags)897 quote(tag) for tag in tags)
871 # Now comes the HAVING clause itself.898 # Now let's build the select itself.
872 select_args['having'] = And(899 second_select = Select(
873 # The list of tags should be a superset of the filter tags to900 BugSubscriptionFilter.id,
874 # be included.901 tables=tables,
875 ArrayContains(902 # Our WHERE clause is straightforward. We are simply
876 SQL(tags_array),903 # focusing on BugSubscriptionFilter.find_all_tags, when the
877 # This next line gives us an array of the tags that the904 # first SELECT did not consider it.
878 # filter wants to include. Notice that it includes the905 where=And(BugSubscriptionFilter.find_all_tags, *conditions),
879 # empty string when the condition does not match, per the906 # The GROUP BY collects the filters together.
880 # discussion above.907 group_by=(BugSubscriptionFilter.id,),
881 ArrayAgg(908 having=And(
882 SQL("CASE WHEN BugSubscriptionFilterTag.include "909 # The list of tags should be a superset of the filter tags to
883 "THEN BugSubscriptionFilterTag.tag "910 # be included.
884 "ELSE ' '::TEXT END"))),911 ArrayContains(
885 # The list of tags should also not intersect with the
886 # tags that the filter wants to exclude.
887 Not(
888 ArrayIntersects(
889 SQL(tags_array),912 SQL(tags_array),
890 # This next line gives us an array of the tags913 # This next line gives us an array of the tags that the
891 # that the filter wants to exclude. We do not bother914 # filter wants to include. Notice that it includes the
892 # with the empty string, and therefore allow NULLs915 # empty string when the condition does not match, per the
893 # into the array, because in this case we are916 # discussion above.
894 # determining whether the sets intersect, not if the
895 # first set subsumes the second.
896 ArrayAgg(917 ArrayAgg(
897 SQL('CASE WHEN '918 SQL("CASE WHEN BugSubscriptionFilterTag.include "
898 'NOT BugSubscriptionFilterTag.include '919 "THEN BugSubscriptionFilterTag.tag "
899 'THEN BugSubscriptionFilterTag.tag END')))))920 "ELSE ' '::TEXT END"))),
900 # Everything is ready. Make our second SELECT statement, UNION921 # The list of tags should also not intersect with the
901 # it, and return it.922 # tags that the filter wants to exclude.
902 return candidates, Union(923 Not(
903 first_select,924 ArrayIntersects(
904 Select(925 SQL(tags_array),
905 BugSubscriptionFilter.id,926 # This next line gives us an array of the tags
906 **select_args))927 # that the filter wants to exclude. We do not bother
928 # with the empty string, and therefore allow NULLs
929 # into the array, because in this case we are
930 # determining whether the sets intersect, not if the
931 # first set subsumes the second.
932 ArrayAgg(
933 SQL('CASE WHEN '
934 'NOT BugSubscriptionFilterTag.include '
935 'THEN BugSubscriptionFilterTag.tag END'))))))
936 # Everything is ready. Return the union.
937 return Union(first_select, second_select)
907938
=== modified file 'lib/lp/bugs/subscribers/bug.py'
--- lib/lp/bugs/subscribers/bug.py 2011-02-11 22:04:25 +0000
+++ lib/lp/bugs/subscribers/bug.py 2011-03-07 20:07:18 +0000
@@ -5,7 +5,6 @@
5__all__ = [5__all__ = [
6 'add_bug_change_notifications',6 'add_bug_change_notifications',
7 'get_bug_delta',7 'get_bug_delta',
8 'get_bugtask_indirect_subscribers',
9 'notify_bug_attachment_added',8 'notify_bug_attachment_added',
10 'notify_bug_attachment_removed',9 'notify_bug_attachment_removed',
11 'notify_bug_comment_added',10 'notify_bug_comment_added',
@@ -16,9 +15,6 @@
1615
1716
18import datetime17import datetime
19from operator import attrgetter
20
21from zope.component import getUtility
2218
23from canonical.config import config19from canonical.config import config
24from canonical.database.sqlbase import block_implicit_flushes20from canonical.database.sqlbase import block_implicit_flushes
@@ -35,10 +31,10 @@
35 )31 )
36from lp.bugs.adapters.bugdelta import BugDelta32from lp.bugs.adapters.bugdelta import BugDelta
37from lp.bugs.enum import BugNotificationLevel33from lp.bugs.enum import BugNotificationLevel
38from lp.bugs.interfaces.bugtask import IBugTaskSet
39from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder34from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
40from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients35from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
41from lp.bugs.mail.newbug import generate_bug_add_email36from lp.bugs.mail.newbug import generate_bug_add_email
37from lp.bugs.model.bug import get_also_notified_subscribers
42from lp.registry.interfaces.person import IPerson38from lp.registry.interfaces.person import IPerson
4339
4440
@@ -146,46 +142,6 @@
146 return None142 return None
147143
148144
149def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
150 """Return the indirect subscribers for a bug task.
151
152 Return the list of people who should get notifications about
153 changes to the task because of having an indirect subscription
154 relationship with it (by subscribing to its target, being an
155 assignee or owner, etc...)
156
157 If `recipients` is present, add the subscribers to the set of
158 bug notification recipients.
159 """
160 if bugtask.bug.private:
161 return set()
162
163 also_notified_subscribers = set()
164
165 # Assignees are indirect subscribers.
166 if bugtask.assignee:
167 also_notified_subscribers.add(bugtask.assignee)
168 if recipients is not None:
169 recipients.addAssignee(bugtask.assignee)
170
171 # Get structural subscribers.
172 also_notified_subscribers.update(
173 getUtility(IBugTaskSet).getStructuralSubscribers(
174 [bugtask], recipients, level))
175
176 # If the target's bug supervisor isn't set,
177 # we add the owner as a subscriber.
178 pillar = bugtask.pillar
179 if pillar.bug_supervisor is None and pillar.official_malone:
180 also_notified_subscribers.add(pillar.owner)
181 if recipients is not None:
182 recipients.addRegistrant(pillar.owner, pillar)
183
184 return sorted(
185 also_notified_subscribers,
186 key=attrgetter('displayname'))
187
188
189def add_bug_change_notifications(bug_delta, old_bugtask=None,145def add_bug_change_notifications(bug_delta, old_bugtask=None,
190 new_subscribers=None):146 new_subscribers=None):
191 """Generate bug notifications and add them to the bug."""147 """Generate bug notifications and add them to the bug."""
@@ -195,7 +151,7 @@
195 level=BugNotificationLevel.METADATA)151 level=BugNotificationLevel.METADATA)
196 if old_bugtask is not None:152 if old_bugtask is not None:
197 old_bugtask_recipients = BugNotificationRecipients()153 old_bugtask_recipients = BugNotificationRecipients()
198 get_bugtask_indirect_subscribers(154 get_also_notified_subscribers(
199 old_bugtask, recipients=old_bugtask_recipients,155 old_bugtask, recipients=old_bugtask_recipients,
200 level=BugNotificationLevel.METADATA)156 level=BugNotificationLevel.METADATA)
201 recipients.update(old_bugtask_recipients)157 recipients.update(old_bugtask_recipients)