Merge lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085-devel into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 11794
Proposed branch: lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085-devel
Merge into: lp:launchpad
Diff against target: 1526 lines (+525/-698)
15 files modified
lib/lp/bugs/configure.zcml (+0/-1)
lib/lp/bugs/doc/bugtask-search.txt (+1/-1)
lib/lp/bugs/interfaces/bug.py (+0/-7)
lib/lp/bugs/interfaces/bugtask.py (+6/-0)
lib/lp/bugs/model/bug.py (+5/-60)
lib/lp/bugs/model/bugtask.py (+59/-1)
lib/lp/bugs/model/tests/test_bug.py (+0/-85)
lib/lp/bugs/model/tests/test_bugtask.py (+442/-2)
lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt (+5/-5)
lib/lp/bugs/subscribers/bug.py (+7/-12)
lib/lp/bugs/tests/test_bugtask_0.py (+0/-36)
lib/lp/bugs/tests/test_bugtask_1.py (+0/-345)
lib/lp/registry/doc/structural-subscriptions.txt (+0/-111)
lib/lp/registry/interfaces/structuralsubscription.py (+0/-14)
lib/lp/registry/model/structuralsubscription.py (+0/-18)
To merge this branch: bzr merge lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085-devel
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+39277@code.launchpad.net

Commit message

Remove getBugNotificationRecipients()

Description of the change

This is the same change as reviewed in:

https://code.edge.launchpad.net/~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085/+merge/38325

However, that branch turned into conflict hell so I decided to
manually apply its revisions to a fresh branch and fix things as I
went. I also decided to re-target it to devel (there were reasons the
previous branch was targeted to db-devel, but they're no longer
relevant).

I've confirmed that the diff of this branch is almost exactly the same
as that in the previous review with the differences worthy of mention:

- In lib/lp/registry/doc/structural-subscriptions.txt an addional
  couple of paragraphs of set-up has been removed. It does nothing
  because the set-up was for some other removed code.

- Some code in the previous merge proposal was unreviewed - I did it
  after Graham completed the review - so that does need
  reviewing. It's fairly short, and in this branch the changes are
  revisions 11800 to 11803 inclusive. Here'a s handy diff I prepared
  earlier: http://paste.ubuntu.com/519697/

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

<henninge> allenap: I have never seen a doc test import from a unit test. Is that a good(tm) practice?
<allenap> henninge: I don't think it's a problem, but it is a bit odd. Certainly BugTaskSearchBugsElsewhereTest.__init__() smells bad.
<allenap> henninge: Arguable those helper methods should have been put in a mixin or just a separate class, but as it is it's reasonably easy to figure out what's going on. I don't think there's much harm in it.
<henninge> Argh! Now I get what that is doing.
<henninge> allenap: Luckily it's not part of this review ... ;-)
<allenap> henninge: Yes, that's what I thought :)
<henninge> allenap: could you call the result "naked_bugtasks" here?
<henninge> + bugtasks = [removeSecurityProxy(bugtask) for bugtask in bugtasks]
<allenap> henninge: Sure.
<henninge> I think there was an agreement to clearly mark naked entities as such ...
<henninge> allenap: r=me for the handy diff. ;-)

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/configure.zcml'
2--- lib/lp/bugs/configure.zcml 2010-10-19 21:30:53 +0000
3+++ lib/lp/bugs/configure.zcml 2010-10-25 19:52:46 +0000
4@@ -706,7 +706,6 @@
5 getSubscribersForPerson
6 indexed_messages
7 getAlsoNotifiedSubscribers
8- getStructuralSubscribers
9 getBugWatch
10 canBeNominatedFor
11 getNominationFor
12
13=== modified file 'lib/lp/bugs/doc/bugtask-search.txt'
14--- lib/lp/bugs/doc/bugtask-search.txt 2010-10-19 18:44:31 +0000
15+++ lib/lp/bugs/doc/bugtask-search.txt 2010-10-25 19:52:46 +0000
16@@ -299,7 +299,7 @@
17 ... BugTaskImportance,
18 ... BugTaskStatus,
19 ... )
20- >>> from lp.bugs.tests.test_bugtask_1 import (
21+ >>> from lp.bugs.model.tests.test_bugtask import (
22 ... BugTaskSearchBugsElsewhereTest)
23 >>> def bugTaskInfo(bugtask):
24 ... return '%i %i %s %s' % (
25
26=== modified file 'lib/lp/bugs/interfaces/bug.py'
27--- lib/lp/bugs/interfaces/bug.py 2010-10-15 16:09:18 +0000
28+++ lib/lp/bugs/interfaces/bug.py 2010-10-25 19:52:46 +0000
29@@ -81,7 +81,6 @@
30 from lp.bugs.interfaces.bugwatch import IBugWatch
31 from lp.bugs.interfaces.cve import ICve
32 from lp.code.interfaces.branchlink import IHasLinkedBranches
33-from lp.registry.enum import BugNotificationLevel
34 from lp.registry.interfaces.mentoringoffer import ICanBeMentored
35 from lp.registry.interfaces.person import IPerson
36 from lp.services.fields import (
37@@ -493,12 +492,6 @@
38 from duplicates.
39 """
40
41- def getStructuralSubscribers(recipients=None, level=None):
42- """Return `IPerson`s subscribed to this bug's targets.
43-
44- This takes into account bug subscription filters.
45- """
46-
47 def getSubscriptionsFromDuplicates():
48 """Return IBugSubscriptions subscribed from dupes of this bug."""
49
50
51=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
52--- lib/lp/bugs/interfaces/bugtask.py 2010-10-22 21:38:42 +0000
53+++ lib/lp/bugs/interfaces/bugtask.py 2010-10-25 19:52:46 +0000
54@@ -1524,6 +1524,12 @@
55 def getOpenBugTasksPerProduct(user, products):
56 """Return open bugtask count for multiple products."""
57
58+ def getStructuralSubscribers(bugtasks, recipients=None, level=None):
59+ """Return `IPerson`s subscribed to the given bug tasks.
60+
61+ This takes into account bug subscription filters.
62+ """
63+
64
65 def valid_remote_bug_url(value):
66 """Verify that the URL is to a bug to a known bug tracker."""
67
68=== modified file 'lib/lp/bugs/model/bug.py'
69--- lib/lp/bugs/model/bug.py 2010-10-19 21:30:53 +0000
70+++ lib/lp/bugs/model/bug.py 2010-10-25 19:52:46 +0000
71@@ -176,9 +176,6 @@
72 from lp.registry.interfaces.productseries import IProductSeries
73 from lp.registry.interfaces.series import SeriesStatus
74 from lp.registry.interfaces.sourcepackage import ISourcePackage
75-from lp.registry.interfaces.structuralsubscription import (
76- IStructuralSubscriptionTarget,
77- )
78 from lp.registry.model.mentoringoffer import MentoringOffer
79 from lp.registry.model.person import (
80 Person,
81@@ -934,8 +931,9 @@
82 # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a
83 # hack; it does not seem to be possible to express DISTINCT ON
84 # with Storm.
85- (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"),
86- # return people and subscribptions
87+ (SQL("DISTINCT ON (Person.name, BugSubscription.person) "
88+ "0 AS ignore"),
89+ # Return people and subscriptions
90 Person, BugSubscription),
91 # For this bug or its duplicates
92 Or(
93@@ -986,8 +984,8 @@
94
95 # Structural subscribers.
96 also_notified_subscribers.update(
97- self.getStructuralSubscribers(
98- recipients=recipients, level=level))
99+ getUtility(IBugTaskSet).getStructuralSubscribers(
100+ self.bugtasks, recipients=recipients, level=level))
101
102 # Direct subscriptions always take precedence over indirect
103 # subscriptions.
104@@ -999,58 +997,6 @@
105 (also_notified_subscribers - direct_subscribers),
106 key=lambda x: removeSecurityProxy(x).displayname)
107
108- def getStructuralSubscribers(self, recipients=None, level=None):
109- """See `IBug`. """
110- query_arguments = []
111- for bugtask in self.bugtasks:
112- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
113- query_arguments.append((bugtask.target, bugtask))
114- if bugtask.target.parent_subscription_target is not None:
115- query_arguments.append(
116- (bugtask.target.parent_subscription_target, bugtask))
117- if ISourcePackage.providedBy(bugtask.target):
118- # Distribution series bug tasks with a package have the source
119- # package set as their target, so we add the distroseries
120- # explicitly to the set of subscription targets.
121- query_arguments.append((bugtask.distroseries, bugtask))
122- if bugtask.milestone is not None:
123- query_arguments.append((bugtask.milestone, bugtask))
124-
125- if len(query_arguments) == 0:
126- return EmptyResultSet()
127-
128- if level is None:
129- # If level is not specified, default to NOTHING so that all
130- # subscriptions are found. XXX: Perhaps this should go in
131- # getSubscriptionsForBugTask()?
132- level = BugNotificationLevel.NOTHING
133-
134- # Build the query.
135- union = lambda left, right: left.union(right)
136- queries = (
137- target.getSubscriptionsForBugTask(bugtask, level)
138- for target, bugtask in query_arguments)
139- subscriptions = reduce(union, queries)
140-
141- # Pull all the subscriptions in.
142- subscriptions = list(subscriptions)
143-
144- # Prepare a query for the subscribers.
145- subscribers = Store.of(self).find(
146- Person, Person.id.is_in(
147- subscription.subscriberID
148- for subscription in subscriptions))
149-
150- if recipients is not None:
151- # We need to process subscriptions, so pull all the subscribes
152- # into the cache, then update recipients with the subscriptions.
153- subscribers = list(subscribers)
154- for subscription in subscriptions:
155- recipients.addStructuralSubscriber(
156- subscription.subscriber, subscription.target)
157-
158- return subscribers
159-
160 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
161 level=None,
162 include_master_dupe_subscribers=False):
163@@ -1541,7 +1487,6 @@
164 assert IProductSeries.providedBy(target)
165 productseries = target
166
167- admins = getUtility(ILaunchpadCelebrities).admin
168 if not (check_permission("launchpad.BugSupervisor", target) or
169 check_permission("launchpad.Driver", target)):
170 raise NominationError(
171
172=== modified file 'lib/lp/bugs/model/bugtask.py'
173--- lib/lp/bugs/model/bugtask.py 2010-10-22 19:56:26 +0000
174+++ lib/lp/bugs/model/bugtask.py 2010-10-25 19:52:46 +0000
175@@ -127,6 +127,7 @@
176 )
177 from lp.bugs.model.bugnomination import BugNomination
178 from lp.bugs.model.bugsubscription import BugSubscription
179+from lp.registry.enum import BugNotificationLevel
180 from lp.registry.interfaces.distribution import (
181 IDistribution,
182 IDistributionSet,
183@@ -155,6 +156,9 @@
184 from lp.registry.interfaces.projectgroup import IProjectGroup
185 from lp.registry.interfaces.sourcepackage import ISourcePackage
186 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
187+from lp.registry.interfaces.structuralsubscription import (
188+ IStructuralSubscriptionTarget,
189+ )
190 from lp.registry.model.pillar import pillar_sort_key
191 from lp.registry.model.sourcepackagename import SourcePackageName
192 from lp.services.propertycache import IPropertyCache
193@@ -1953,7 +1957,7 @@
194 query = " AND ".join(extra_clauses)
195
196 if not decorators:
197- decorator = lambda x:x
198+ decorator = lambda x: x
199 else:
200 def decorator(obj):
201 for decor in decorators:
202@@ -2835,3 +2839,57 @@
203 counts.append(package_counts)
204
205 return counts
206+
207+ def getStructuralSubscribers(self, bugtasks, recipients=None, level=None):
208+ """See `IBugTaskSet`."""
209+ query_arguments = []
210+ for bugtask in bugtasks:
211+ if IStructuralSubscriptionTarget.providedBy(bugtask.target):
212+ query_arguments.append((bugtask.target, bugtask))
213+ if bugtask.target.parent_subscription_target is not None:
214+ query_arguments.append(
215+ (bugtask.target.parent_subscription_target, bugtask))
216+ if ISourcePackage.providedBy(bugtask.target):
217+ # Distribution series bug tasks with a package have the source
218+ # package set as their target, so we add the distroseries
219+ # explicitly to the set of subscription targets.
220+ query_arguments.append((bugtask.distroseries, bugtask))
221+ if bugtask.milestone is not None:
222+ query_arguments.append((bugtask.milestone, bugtask))
223+
224+ if len(query_arguments) == 0:
225+ return EmptyResultSet()
226+
227+ if level is None:
228+ # If level is not specified, default to NOTHING so that all
229+ # subscriptions are found.
230+ level = BugNotificationLevel.NOTHING
231+
232+ # Build the query.
233+ union = lambda left, right: (
234+ removeSecurityProxy(left).union(
235+ removeSecurityProxy(right)))
236+ queries = (
237+ target.getSubscriptionsForBugTask(bugtask, level)
238+ for target, bugtask in query_arguments)
239+ subscriptions = reduce(union, queries)
240+
241+ # Pull all the subscriptions in.
242+ subscriptions = list(subscriptions)
243+
244+ # Prepare a query for the subscribers.
245+ from lp.registry.model.person import Person
246+ subscribers = IStore(Person).find(
247+ Person, Person.id.is_in(
248+ removeSecurityProxy(subscription).subscriberID
249+ for subscription in subscriptions))
250+
251+ if recipients is not None:
252+ # We need to process subscriptions, so pull all the subscribes into
253+ # the cache, then update recipients with the subscriptions.
254+ subscribers = list(subscribers)
255+ for subscription in subscriptions:
256+ recipients.addStructuralSubscriber(
257+ subscription.subscriber, subscription.target)
258+
259+ return subscribers
260
261=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
262--- lib/lp/bugs/model/tests/test_bug.py 2010-10-15 16:11:17 +0000
263+++ lib/lp/bugs/model/tests/test_bug.py 2010-10-25 19:52:46 +0000
264@@ -5,10 +5,7 @@
265
266 __metaclass__ = type
267
268-from storm.store import ResultSet
269-
270 from canonical.testing.layers import DatabaseFunctionalLayer
271-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
272 from lp.registry.enum import BugNotificationLevel
273 from lp.registry.interfaces.person import PersonVisibility
274 from lp.registry.model.structuralsubscription import StructuralSubscription
275@@ -17,7 +14,6 @@
276 person_logged_in,
277 TestCaseWithFactory,
278 )
279-from lp.testing.matchers import StartsWith
280
281
282 class TestBug(TestCaseWithFactory):
283@@ -246,84 +242,3 @@
284 self.assertTrue(
285 subscriber not in duplicate_subscribers,
286 "Subscriber should not be in duplicate_subscribers.")
287-
288-
289-class TestBugStructuralSubscribers(TestCaseWithFactory):
290-
291- layer = DatabaseFunctionalLayer
292-
293- def test_getStructuralSubscribers_no_subscribers(self):
294- # If there are no subscribers for any of the bug's targets then no
295- # subscribers will be returned by getStructuralSubscribers().
296- product = self.factory.makeProduct()
297- bug = self.factory.makeBug(product=product)
298- subscribers = bug.getStructuralSubscribers()
299- self.assertIsInstance(subscribers, ResultSet)
300- self.assertEqual([], list(subscribers))
301-
302- def test_getStructuralSubscribers_single_target(self):
303- # Subscribers for any of the bug's targets are returned.
304- subscriber = self.factory.makePerson()
305- login_person(subscriber)
306- product = self.factory.makeProduct()
307- product.addBugSubscription(subscriber, subscriber)
308- bug = self.factory.makeBug(product=product)
309- self.assertEqual([subscriber], list(bug.getStructuralSubscribers()))
310-
311- def test_getStructuralSubscribers_multiple_targets(self):
312- # Subscribers for any of the bug's targets are returned.
313- actor = self.factory.makePerson()
314- login_person(actor)
315-
316- subscriber1 = self.factory.makePerson()
317- subscriber2 = self.factory.makePerson()
318-
319- product1 = self.factory.makeProduct(owner=actor)
320- product1.addBugSubscription(subscriber1, subscriber1)
321- product2 = self.factory.makeProduct(owner=actor)
322- product2.addBugSubscription(subscriber2, subscriber2)
323-
324- bug = self.factory.makeBug(product=product1)
325- bug.addTask(actor, product2)
326-
327- subscribers = bug.getStructuralSubscribers()
328- self.assertIsInstance(subscribers, ResultSet)
329- self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
330-
331- def test_getStructuralSubscribers_recipients(self):
332- # If provided, getStructuralSubscribers() calls the appropriate
333- # methods on a BugNotificationRecipients object.
334- subscriber = self.factory.makePerson()
335- login_person(subscriber)
336- product = self.factory.makeProduct()
337- product.addBugSubscription(subscriber, subscriber)
338- bug = self.factory.makeBug(product=product)
339- recipients = BugNotificationRecipients()
340- subscribers = bug.getStructuralSubscribers(recipients=recipients)
341- # The return value is a list only when populating recipients.
342- self.assertIsInstance(subscribers, list)
343- self.assertEqual([subscriber], recipients.getRecipients())
344- reason, header = recipients.getReason(subscriber)
345- self.assertThat(
346- reason, StartsWith(
347- u"You received this bug notification because "
348- u"you are subscribed to "))
349- self.assertThat(header, StartsWith(u"Subscriber "))
350-
351- def test_getStructuralSubscribers_level(self):
352- # getStructuralSubscribers() respects the given level.
353- subscriber = self.factory.makePerson()
354- login_person(subscriber)
355- product = self.factory.makeProduct()
356- subscription = product.addBugSubscription(subscriber, subscriber)
357- subscription.bug_notification_level = BugNotificationLevel.METADATA
358- bug = self.factory.makeBug(product=product)
359- self.assertEqual(
360- [subscriber], list(
361- bug.getStructuralSubscribers(
362- level=BugNotificationLevel.METADATA)))
363- subscription.bug_notification_level = BugNotificationLevel.METADATA
364- self.assertEqual(
365- [], list(
366- bug.getStructuralSubscribers(
367- level=BugNotificationLevel.COMMENTS)))
368
369=== renamed file 'lib/lp/bugs/tests/test_bugtask.py' => 'lib/lp/bugs/model/tests/test_bugtask.py'
370--- lib/lp/bugs/tests/test_bugtask.py 2010-10-21 16:40:10 +0000
371+++ lib/lp/bugs/model/tests/test_bugtask.py 2010-10-25 19:52:46 +0000
372@@ -8,31 +8,52 @@
373 import unittest
374
375 from lazr.lifecycle.snapshot import Snapshot
376+from storm.store import ResultSet
377 from zope.component import getUtility
378 from zope.interface import providedBy
379
380+from canonical.database.sqlbase import flush_database_updates
381 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
382 from canonical.launchpad.searchbuilder import (
383 all,
384 any,
385 )
386+from canonical.launchpad.webapp.interfaces import ILaunchBag
387 from canonical.testing.layers import (
388 DatabaseFunctionalLayer,
389 LaunchpadZopelessLayer,
390 )
391+from lp.app.enums import ServiceUsage
392+from lp.bugs.interfaces.bug import IBugSet
393 from lp.bugs.interfaces.bugtarget import IBugTarget
394 from lp.bugs.interfaces.bugtask import (
395 BugTaskImportance,
396 BugTaskSearchParams,
397 BugTaskStatus,
398+ IBugTaskSet,
399+ IUpstreamBugTask,
400+ RESOLVED_BUGTASK_STATUSES,
401+ UNRESOLVED_BUGTASK_STATUSES,
402 )
403+from lp.bugs.interfaces.bugwatch import IBugWatchSet
404+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
405 from lp.bugs.model.bugtask import build_tag_search_clause
406+from lp.bugs.tests.bug import (
407+ create_old_bug,
408+ sync_bugtasks,
409+ )
410 from lp.hardwaredb.interfaces.hwdb import (
411 HWBus,
412 IHWDeviceSet,
413 )
414+from lp.registry.enum import BugNotificationLevel
415 from lp.registry.interfaces.distribution import IDistributionSet
416-from lp.registry.interfaces.person import IPerson, IPersonSet
417+from lp.registry.interfaces.person import (
418+ IPerson,
419+ IPersonSet,
420+ )
421+from lp.registry.interfaces.product import IProductSet
422+from lp.registry.interfaces.projectgroup import IProjectGroupSet
423 from lp.testing import (
424 ANONYMOUS,
425 login,
426@@ -42,6 +63,11 @@
427 TestCase,
428 TestCaseWithFactory,
429 )
430+from lp.testing.factory import (
431+ is_security_proxied_or_harmless,
432+ LaunchpadObjectFactory,
433+ )
434+from lp.testing.matchers import StartsWith
435
436
437 class TestBugTaskDelta(TestCaseWithFactory):
438@@ -892,7 +918,7 @@
439 self.assertEqual(2, tasks.count())
440 # Cache in the storm cache the account->person lookup so its not
441 # distorting what we're testing.
442- _ = IPerson(person.account, None)
443+ IPerson(person.account, None)
444 # One query and only one should be issued to get the tasks, bugs and
445 # allow access to getConjoinedMaster attribute - an attribute that
446 # triggers a permission check (nb: id does not trigger such a check)
447@@ -945,6 +971,420 @@
448 self.assertEqual([task2], list(result))
449
450
451+class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
452+ """Tests for searching bugs filtering on related bug tasks.
453+
454+ It also acts as a helper class, which makes related doctests more
455+ readable, since they can use methods from this class.
456+ """
457+ layer = DatabaseFunctionalLayer
458+
459+ def __init__(self, methodName='runTest', helper_only=False):
460+ """If helper_only is True, set up it only as a helper class."""
461+ if not helper_only:
462+ unittest.TestCase.__init__(self, methodName=methodName)
463+
464+ def setUp(self):
465+ login(ANONYMOUS)
466+
467+ def tearDown(self):
468+ logout()
469+
470+ def _getBugTaskByTarget(self, bug, target):
471+ """Return a bug's bugtask for the given target."""
472+ for bugtask in bug.bugtasks:
473+ if bugtask.target == target:
474+ return bugtask
475+ else:
476+ raise AssertionError(
477+ "Didn't find a %s task on bug %s." % (
478+ target.bugtargetname, bug.id))
479+
480+ def setUpBugsResolvedUpstreamTests(self):
481+ """Modify some bugtasks to match the resolved upstream filter."""
482+ bugset = getUtility(IBugSet)
483+ productset = getUtility(IProductSet)
484+ firefox = productset.getByName("firefox")
485+ thunderbird = productset.getByName("thunderbird")
486+
487+ # Mark an upstream task on bug #1 "Fix Released"
488+ bug_one = bugset.get(1)
489+ firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
490+ self.assertEqual(
491+ ServiceUsage.LAUNCHPAD,
492+ firefox_upstream.product.bug_tracking_usage)
493+ self.old_firefox_status = firefox_upstream.status
494+ firefox_upstream.transitionToStatus(
495+ BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
496+ self.firefox_upstream = firefox_upstream
497+
498+ # Mark an upstream task on bug #9 "Fix Committed"
499+ bug_nine = bugset.get(9)
500+ thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
501+ self.old_thunderbird_status = thunderbird_upstream.status
502+ thunderbird_upstream.transitionToStatus(
503+ BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
504+ self.thunderbird_upstream = thunderbird_upstream
505+
506+ # Add a watch to a Debian bug for bug #2, and mark the task Fix
507+ # Released.
508+ bug_two = bugset.get(2)
509+ bugwatchset = getUtility(IBugWatchSet)
510+
511+ # Get a debbugs watch.
512+ watch_debbugs_327452 = bugwatchset.get(9)
513+ self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
514+ self.assertEquals(watch_debbugs_327452.remotebug, "327452")
515+
516+ # Associate the watch to a Fix Released task.
517+ debian = getUtility(IDistributionSet).getByName("debian")
518+ debian_firefox = debian.getSourcePackage("mozilla-firefox")
519+ bug_two_in_debian_firefox = self._getBugTaskByTarget(
520+ bug_two, debian_firefox)
521+ bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
522+ bug_two_in_debian_firefox.transitionToStatus(
523+ BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
524+
525+ flush_database_updates()
526+
527+ def tearDownBugsElsewhereTests(self):
528+ """Resets the modified bugtasks to their original statuses."""
529+ self.firefox_upstream.transitionToStatus(
530+ self.old_firefox_status,
531+ self.firefox_upstream.target.bug_supervisor)
532+ self.thunderbird_upstream.transitionToStatus(
533+ self.old_thunderbird_status,
534+ self.firefox_upstream.target.bug_supervisor)
535+ flush_database_updates()
536+
537+ def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
538+ """Assert the bugtask is pending a bug watch elsewhere.
539+
540+ Pending a bugwatch elsewhere means that at least one of the bugtask's
541+ related task's target isn't using Malone, and that
542+ related_bugtask.bugwatch is None.
543+ """
544+ non_malone_using_bugtasks = [
545+ related_task for related_task in bugtask.related_tasks
546+ if not related_task.target_uses_malone]
547+ pending_bugwatch_bugtasks = [
548+ related_bugtask for related_bugtask in non_malone_using_bugtasks
549+ if related_bugtask.bugwatch is None]
550+ self.assert_(
551+ len(pending_bugwatch_bugtasks) > 0,
552+ 'Bugtask %s on %s has no related bug watches elsewhere.' % (
553+ bugtask.id, bugtask.target.displayname))
554+
555+ def assertBugTaskIsResolvedUpstream(self, bugtask):
556+ """Make sure at least one of the related upstream tasks is resolved.
557+
558+ "Resolved", for our purposes, means either that one of the related
559+ tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
560+ it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
561+ INVALID state.
562+ """
563+ resolved_upstream_states = [
564+ BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
565+ resolved_bugwatch_states = [
566+ BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
567+ BugTaskStatus.INVALID]
568+
569+ # Helper functions for the list comprehension below.
570+ def _is_resolved_upstream_task(bugtask):
571+ return (
572+ IUpstreamBugTask.providedBy(bugtask) and
573+ bugtask.status in resolved_upstream_states)
574+
575+ def _is_resolved_bugwatch_task(bugtask):
576+ return (
577+ bugtask.bugwatch and bugtask.status in
578+ resolved_bugwatch_states)
579+
580+ resolved_related_tasks = [
581+ related_task for related_task in bugtask.related_tasks
582+ if (_is_resolved_upstream_task(related_task) or
583+ _is_resolved_bugwatch_task(related_task))]
584+
585+ self.assert_(len(resolved_related_tasks) > 0)
586+ self.assert_(
587+ len(resolved_related_tasks) > 0,
588+ 'Bugtask %s on %s has no resolved related tasks.' % (
589+ bugtask.id, bugtask.target.displayname))
590+
591+ def assertBugTaskIsOpenUpstream(self, bugtask):
592+ """Make sure at least one of the related upstream tasks is open.
593+
594+ "Open", for our purposes, means either that one of the related
595+ tasks is an upstream task or a task with a bugwatch which has
596+ one of the states listed in open_states.
597+ """
598+ open_states = [
599+ BugTaskStatus.NEW,
600+ BugTaskStatus.INCOMPLETE,
601+ BugTaskStatus.CONFIRMED,
602+ BugTaskStatus.INPROGRESS,
603+ BugTaskStatus.UNKNOWN]
604+
605+ # Helper functions for the list comprehension below.
606+ def _is_open_upstream_task(bugtask):
607+ return (
608+ IUpstreamBugTask.providedBy(bugtask) and
609+ bugtask.status in open_states)
610+
611+ def _is_open_bugwatch_task(bugtask):
612+ return (
613+ bugtask.bugwatch and bugtask.status in
614+ open_states)
615+
616+ open_related_tasks = [
617+ related_task for related_task in bugtask.related_tasks
618+ if (_is_open_upstream_task(related_task) or
619+ _is_open_bugwatch_task(related_task))]
620+
621+ self.assert_(
622+ len(open_related_tasks) > 0,
623+ 'Bugtask %s on %s has no open related tasks.' % (
624+ bugtask.id, bugtask.target.displayname))
625+
626+ def _hasUpstreamTask(self, bug):
627+ """Does this bug have an upstream task associated with it?
628+
629+ Returns True if yes, otherwise False.
630+ """
631+ for bugtask in bug.bugtasks:
632+ if IUpstreamBugTask.providedBy(bugtask):
633+ return True
634+ return False
635+
636+ def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
637+ """Should the bugtask be shown in the search no upstream task search?
638+
639+ Returns True if yes, otherwise False.
640+ """
641+ self.assert_(
642+ not self._hasUpstreamTask(bugtask.bug),
643+ 'Bugtask %s on %s has upstream tasks.' % (
644+ bugtask.id, bugtask.target.displayname))
645+
646+
647+class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
648+ """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
649+ layer = DatabaseFunctionalLayer
650+
651+ def setUp(self):
652+ """Setup the zope interaction and create expirable bugtasks."""
653+ login('test@canonical.com')
654+ self.user = getUtility(ILaunchBag).user
655+ self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
656+ self.distroseries = self.distribution.getSeries('hoary')
657+ self.product = getUtility(IProductSet).getByName('jokosher')
658+ self.productseries = self.product.getSeries('trunk')
659+ self.bugtaskset = getUtility(IBugTaskSet)
660+ bugtasks = []
661+ bugtasks.append(
662+ create_old_bug("90 days old", 90, self.distribution))
663+ bugtasks.append(
664+ self.bugtaskset.createTask(
665+ bug=bugtasks[-1].bug, owner=self.user,
666+ distroseries=self.distroseries))
667+ bugtasks.append(
668+ create_old_bug("90 days old", 90, self.product))
669+ bugtasks.append(
670+ self.bugtaskset.createTask(
671+ bug=bugtasks[-1].bug, owner=self.user,
672+ productseries=self.productseries))
673+ sync_bugtasks(bugtasks)
674+
675+ def tearDown(self):
676+ logout()
677+
678+ def testSupportedTargetParam(self):
679+ """The target param supports a limited set of BugTargets.
680+
681+ Four BugTarget types may passed as the target argument:
682+ Distribution, DistroSeries, Product, ProductSeries.
683+ """
684+ supported_targets = [self.distribution, self.distroseries,
685+ self.product, self.productseries]
686+ for target in supported_targets:
687+ expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
688+ 0, self.user, target=target)
689+ self.assertNotEqual(expirable_bugtasks.count(), 0,
690+ "%s has %d expirable bugtasks." %
691+ (self.distroseries, expirable_bugtasks.count()))
692+
693+ def testUnsupportedBugTargetParam(self):
694+ """Test that unsupported targets raise errors.
695+
696+ Three BugTarget types are not supported because the UI does not
697+ provide bug-index to link to the 'bugs that can expire' page.
698+ ProjectGroup, SourcePackage, and DistributionSourcePackage will
699+ raise an NotImplementedError.
700+
701+ Passing an unknown bugtarget type will raise an AssertionError.
702+ """
703+ project = getUtility(IProjectGroupSet).getByName('mozilla')
704+ distributionsourcepackage = self.distribution.getSourcePackage(
705+ 'mozilla-firefox')
706+ sourcepackage = self.distroseries.getSourcePackage(
707+ 'mozilla-firefox')
708+ unsupported_targets = [project, distributionsourcepackage,
709+ sourcepackage]
710+ for target in unsupported_targets:
711+ self.assertRaises(
712+ NotImplementedError, self.bugtaskset.findExpirableBugTasks,
713+ 0, self.user, target=target)
714+
715+ # Objects that are not a known BugTarget type raise an AssertionError.
716+ self.assertRaises(
717+ AssertionError, self.bugtaskset.findExpirableBugTasks,
718+ 0, self.user, target=[])
719+
720+
721+class BugTaskSetTest(unittest.TestCase):
722+ """Test `BugTaskSet` methods."""
723+ layer = DatabaseFunctionalLayer
724+
725+ def setUp(self):
726+ login(ANONYMOUS)
727+
728+ def test_getBugTasks(self):
729+ """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
730+ bugs to their bugtasks. It does that in a single query, to avoid
731+ hitting the DB again when getting the bugs' tasks.
732+ """
733+ login('no-priv@canonical.com')
734+ factory = LaunchpadObjectFactory()
735+ bug1 = factory.makeBug()
736+ factory.makeBugTask(bug1)
737+ bug2 = factory.makeBug()
738+ factory.makeBugTask(bug2)
739+ factory.makeBugTask(bug2)
740+
741+ bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
742+ [bug1.id, bug2.id])
743+ # The bugtasks returned by getBugTasks() are exactly the same as the
744+ # ones returned by bug.bugtasks, obviously.
745+ self.failUnlessEqual(
746+ set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
747+ set([]))
748+ self.failUnlessEqual(
749+ set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
750+ set([]))
751+
752+ def test_getBugTasks_with_empty_list(self):
753+ # When given an empty list of bug IDs, getBugTasks() will return an
754+ # empty dictionary.
755+ bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
756+ self.failUnlessEqual(bugs_and_tasks, {})
757+
758+
759+class TestBugTaskStatuses(TestCase):
760+
761+ def test_open_and_resolved_statuses(self):
762+ """
763+ There are constants that are used to define which statuses are for
764+ resolved bugs (`RESOLVED_BUGTASK_STATUSES`), and which are for
765+ unresolved bugs (`UNRESOLVED_BUGTASK_STATUSES`). The two constants
766+ include all statuses defined in BugTaskStatus, except for Unknown.
767+ """
768+ self.assertNotIn(BugTaskStatus.UNKNOWN, RESOLVED_BUGTASK_STATUSES)
769+ self.assertNotIn(BugTaskStatus.UNKNOWN, UNRESOLVED_BUGTASK_STATUSES)
770+
771+
772+class TestGetStructuralSubscribers(TestCaseWithFactory):
773+
774+ layer = DatabaseFunctionalLayer
775+
776+ def make_product_with_bug(self):
777+ product = self.factory.makeProduct()
778+ bug = self.factory.makeBug(product=product)
779+ return product, bug
780+
781+ def getStructuralSubscribers(self, bugtasks, *args, **kwargs):
782+ # Call IBugTaskSet.getStructuralSubscribers() and check that the
783+ # result is security proxied.
784+ result = getUtility(IBugTaskSet).getStructuralSubscribers(
785+ bugtasks, *args, **kwargs)
786+ self.assertTrue(is_security_proxied_or_harmless(result))
787+ return result
788+
789+ def test_getStructuralSubscribers_no_subscribers(self):
790+ # If there are no subscribers for any of the bug's targets then no
791+ # subscribers will be returned by getStructuralSubscribers().
792+ product, bug = self.make_product_with_bug()
793+ subscribers = self.getStructuralSubscribers(bug.bugtasks)
794+ self.assertIsInstance(subscribers, ResultSet)
795+ self.assertEqual([], list(subscribers))
796+
797+ def test_getStructuralSubscribers_single_target(self):
798+ # Subscribers for any of the bug's targets are returned.
799+ subscriber = self.factory.makePerson()
800+ login_person(subscriber)
801+ product, bug = self.make_product_with_bug()
802+ product.addBugSubscription(subscriber, subscriber)
803+ self.assertEqual(
804+ [subscriber], list(
805+ self.getStructuralSubscribers(bug.bugtasks)))
806+
807+ def test_getStructuralSubscribers_multiple_targets(self):
808+ # Subscribers for any of the bug's targets are returned.
809+ actor = self.factory.makePerson()
810+ login_person(actor)
811+
812+ subscriber1 = self.factory.makePerson()
813+ subscriber2 = self.factory.makePerson()
814+
815+ product1 = self.factory.makeProduct(owner=actor)
816+ product1.addBugSubscription(subscriber1, subscriber1)
817+ product2 = self.factory.makeProduct(owner=actor)
818+ product2.addBugSubscription(subscriber2, subscriber2)
819+
820+ bug = self.factory.makeBug(product=product1)
821+ bug.addTask(actor, product2)
822+
823+ subscribers = self.getStructuralSubscribers(bug.bugtasks)
824+ self.assertIsInstance(subscribers, ResultSet)
825+ self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
826+
827+ def test_getStructuralSubscribers_recipients(self):
828+ # If provided, getStructuralSubscribers() calls the appropriate
829+ # methods on a BugNotificationRecipients object.
830+ subscriber = self.factory.makePerson()
831+ login_person(subscriber)
832+ product, bug = self.make_product_with_bug()
833+ product.addBugSubscription(subscriber, subscriber)
834+ recipients = BugNotificationRecipients()
835+ subscribers = self.getStructuralSubscribers(
836+ bug.bugtasks, recipients=recipients)
837+ # The return value is a list only when populating recipients.
838+ self.assertIsInstance(subscribers, list)
839+ self.assertEqual([subscriber], recipients.getRecipients())
840+ reason, header = recipients.getReason(subscriber)
841+ self.assertThat(
842+ reason, StartsWith(
843+ u"You received this bug notification because "
844+ u"you are subscribed to "))
845+ self.assertThat(header, StartsWith(u"Subscriber "))
846+
847+ def test_getStructuralSubscribers_level(self):
848+ # getStructuralSubscribers() respects the given level.
849+ subscriber = self.factory.makePerson()
850+ login_person(subscriber)
851+ product, bug = self.make_product_with_bug()
852+ subscription = product.addBugSubscription(subscriber, subscriber)
853+ subscription.bug_notification_level = BugNotificationLevel.METADATA
854+ self.assertEqual(
855+ [subscriber], list(
856+ self.getStructuralSubscribers(
857+ bug.bugtasks, level=BugNotificationLevel.METADATA)))
858+ subscription.bug_notification_level = BugNotificationLevel.METADATA
859+ self.assertEqual(
860+ [], list(
861+ self.getStructuralSubscribers(
862+ bug.bugtasks, level=BugNotificationLevel.COMMENTS)))
863+
864+
865 def test_suite():
866 suite = unittest.TestSuite()
867 suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
868
869=== renamed file 'lib/lp/bugs/tests/test_bugtask_status.py' => 'lib/lp/bugs/model/tests/test_bugtask_status.py'
870=== modified file 'lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt'
871--- lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt 2010-10-18 22:24:59 +0000
872+++ lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt 2010-10-25 19:52:46 +0000
873@@ -17,7 +17,7 @@
874
875 Now if we go to the advanced search and choose to list only the bugs
876 needing a bug watch, only the bugs with tasks in other contexts that
877-don't use Launchpad Bugs are shown, if at least one of those contexts
878+don't use Launchpad Bugs are shown, if at least one of those contexts
879 doesn't have a bug watch.
880
881 # XXX: Bjorn Tillenius 2006-07-04 bug=51853:
882@@ -97,7 +97,7 @@
883 demonstrate.
884
885 >>> from canonical.launchpad.ftests import login, logout
886- >>> from lp.bugs.tests.test_bugtask_1 import (
887+ >>> from lp.bugs.model.tests.test_bugtask import (
888 ... BugTaskSearchBugsElsewhereTest)
889 >>> test_helper = BugTaskSearchBugsElsewhereTest(helper_only=True)
890 >>> login('test@canonical.com')
891@@ -181,8 +181,8 @@
892 linux-source-2.6.15 Medium New
893 2 Blackhole Trash folder
894 &mdash; Medium New
895-
896-The user opens a bookmark for "upstream status: Show only bugs that need
897+
898+The user opens a bookmark for "upstream status: Show only bugs that need
899 to be forwarded to an upstream bug tracker".
900
901 >>> bookmark_params['field.status_upstream'] = 'pending_bugwatch'
902@@ -225,7 +225,7 @@
903 ... bookmark_params, True))
904 Traceback (most recent call last):
905 ...
906- UnexpectedFormData: Unexpected value for field 'status_upstream'.
907+ UnexpectedFormData: Unexpected value for field 'status_upstream'.
908 Perhaps your bookmarks are out of date or you changed the URL by hand?
909
910
911
912=== modified file 'lib/lp/bugs/subscribers/bug.py'
913--- lib/lp/bugs/subscribers/bug.py 2010-08-23 09:25:17 +0000
914+++ lib/lp/bugs/subscribers/bug.py 2010-10-25 19:52:46 +0000
915@@ -19,6 +19,8 @@
916 import datetime
917 from operator import attrgetter
918
919+from zope.component import getUtility
920+
921 from canonical.config import config
922 from canonical.database.sqlbase import block_implicit_flushes
923 from canonical.launchpad.helpers import get_contact_email_addresses
924@@ -34,14 +36,12 @@
925 )
926 from lp.bugs.adapters.bugdelta import BugDelta
927 from lp.bugs.interfaces.bugchange import IBugChange
928+from lp.bugs.interfaces.bugtask import IBugTaskSet
929 from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
930 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
931 from lp.bugs.mail.newbug import generate_bug_add_email
932 from lp.registry.enum import BugNotificationLevel
933 from lp.registry.interfaces.person import IPerson
934-from lp.registry.interfaces.structuralsubscription import (
935- IStructuralSubscriptionTarget,
936- )
937
938
939 @block_implicit_flushes
940@@ -179,15 +179,10 @@
941 if recipients is not None:
942 recipients.addAssignee(bugtask.assignee)
943
944- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
945- also_notified_subscribers.update(
946- bugtask.target.getBugNotificationsRecipients(
947- recipients, level=level))
948-
949- if bugtask.milestone is not None:
950- also_notified_subscribers.update(
951- bugtask.milestone.getBugNotificationsRecipients(
952- recipients, level=level))
953+ # Get structural subscribers.
954+ also_notified_subscribers.update(
955+ getUtility(IBugTaskSet).getStructuralSubscribers(
956+ [bugtask], recipients, level))
957
958 # If the target's bug supervisor isn't set,
959 # we add the owner as a subscriber.
960
961=== removed file 'lib/lp/bugs/tests/test_bugtask_0.py'
962--- lib/lp/bugs/tests/test_bugtask_0.py 2010-10-21 04:19:36 +0000
963+++ lib/lp/bugs/tests/test_bugtask_0.py 1970-01-01 00:00:00 +0000
964@@ -1,36 +0,0 @@
965-# Copyright 2009 Canonical Ltd. This software is licensed under the
966-# GNU Affero General Public License version 3 (see the file LICENSE).
967-
968-"""Tests for bugtask.py."""
969-
970-__metaclass__ = type
971-
972-from doctest import (
973- DocTestSuite,
974- ELLIPSIS,
975- NORMALIZE_WHITESPACE,
976- REPORT_NDIFF,
977- )
978-
979-
980-def test_open_and_resolved_statuses(self):
981- """
982- There are constants that are used to define which statuses are for
983- resolved bugs (RESOLVED_BUGTASK_STATUSES), and which are for
984- unresolved bugs (UNRESOLVED_BUGTASK_STATUSES). The two constants
985- include all statuses defined in BugTaskStatus, except for Unknown.
986-
987- >>> from lp.bugs.interfaces.bugtask import (
988- ... BugTaskStatus, RESOLVED_BUGTASK_STATUSES,
989- ... UNRESOLVED_BUGTASK_STATUSES)
990- >>> not_included_status = set(BugTaskStatus.items).difference(
991- ... RESOLVED_BUGTASK_STATUSES + UNRESOLVED_BUGTASK_STATUSES)
992- >>> [status.name for status in not_included_status]
993- ['UNKNOWN']
994- """
995-
996-
997-def test_suite():
998- suite = DocTestSuite(
999- optionflags=REPORT_NDIFF|NORMALIZE_WHITESPACE|ELLIPSIS)
1000- return suite
1001
1002=== removed file 'lib/lp/bugs/tests/test_bugtask_1.py'
1003--- lib/lp/bugs/tests/test_bugtask_1.py 2010-10-21 12:43:32 +0000
1004+++ lib/lp/bugs/tests/test_bugtask_1.py 1970-01-01 00:00:00 +0000
1005@@ -1,345 +0,0 @@
1006-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
1007-# GNU Affero General Public License version 3 (see the file LICENSE).
1008-
1009-"""Bugtask related tests that are too complex to be readable as doctests."""
1010-
1011-__metaclass__ = type
1012-
1013-import unittest
1014-
1015-from zope.component import getUtility
1016-
1017-from canonical.database.sqlbase import flush_database_updates
1018-from canonical.launchpad.ftests import (
1019- ANONYMOUS,
1020- login,
1021- logout,
1022- )
1023-from canonical.launchpad.webapp.interfaces import ILaunchBag
1024-from canonical.testing.layers import DatabaseFunctionalLayer
1025-from lp.app.enums import ServiceUsage
1026-from lp.bugs.interfaces.bug import IBugSet
1027-from lp.bugs.interfaces.bugtask import (
1028- BugTaskStatus,
1029- IBugTaskSet,
1030- IUpstreamBugTask,
1031- )
1032-from lp.bugs.interfaces.bugwatch import IBugWatchSet
1033-from lp.bugs.tests.bug import (
1034- create_old_bug,
1035- sync_bugtasks,
1036- )
1037-from lp.registry.interfaces.distribution import IDistributionSet
1038-from lp.registry.interfaces.product import IProductSet
1039-from lp.registry.interfaces.projectgroup import IProjectGroupSet
1040-from lp.testing.factory import LaunchpadObjectFactory
1041-
1042-
1043-class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
1044- """Tests for searching bugs filtering on related bug tasks.
1045-
1046- It also acts as a helper class, which makes related doctests more
1047- readable, since they can use methods from this class.
1048- """
1049- layer = DatabaseFunctionalLayer
1050-
1051- def __init__(self, methodName='runTest', helper_only=False):
1052- """If helper_only is True, set up it only as a helper class."""
1053- if not helper_only:
1054- unittest.TestCase.__init__(self, methodName=methodName)
1055-
1056- def setUp(self):
1057- login(ANONYMOUS)
1058-
1059- def tearDown(self):
1060- logout()
1061-
1062- def _getBugTaskByTarget(self, bug, target):
1063- """Return a bug's bugtask for the given target."""
1064- for bugtask in bug.bugtasks:
1065- if bugtask.target == target:
1066- return bugtask
1067- else:
1068- raise AssertionError(
1069- "Didn't find a %s task on bug %s." % (
1070- target.bugtargetname, bug.id))
1071-
1072- def setUpBugsResolvedUpstreamTests(self):
1073- """Modify some bugtasks to match the resolved upstream filter."""
1074- bugset = getUtility(IBugSet)
1075- productset = getUtility(IProductSet)
1076- firefox = productset.getByName("firefox")
1077- thunderbird = productset.getByName("thunderbird")
1078-
1079- # Mark an upstream task on bug #1 "Fix Released"
1080- bug_one = bugset.get(1)
1081- firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
1082- self.assertEqual(
1083- ServiceUsage.LAUNCHPAD,
1084- firefox_upstream.product.bug_tracking_usage)
1085- self.old_firefox_status = firefox_upstream.status
1086- firefox_upstream.transitionToStatus(
1087- BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
1088- self.firefox_upstream = firefox_upstream
1089-
1090- # Mark an upstream task on bug #9 "Fix Committed"
1091- bug_nine = bugset.get(9)
1092- thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
1093- self.old_thunderbird_status = thunderbird_upstream.status
1094- thunderbird_upstream.transitionToStatus(
1095- BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
1096- self.thunderbird_upstream = thunderbird_upstream
1097-
1098- # Add a watch to a Debian bug for bug #2, and mark the task Fix
1099- # Released.
1100- bug_two = bugset.get(2)
1101- current_user = getUtility(ILaunchBag).user
1102- bugtaskset = getUtility(IBugTaskSet)
1103- bugwatchset = getUtility(IBugWatchSet)
1104-
1105- # Get a debbugs watch.
1106- watch_debbugs_327452 = bugwatchset.get(9)
1107- self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
1108- self.assertEquals(watch_debbugs_327452.remotebug, "327452")
1109-
1110- # Associate the watch to a Fix Released task.
1111- debian = getUtility(IDistributionSet).getByName("debian")
1112- debian_firefox = debian.getSourcePackage("mozilla-firefox")
1113- bug_two_in_debian_firefox = self._getBugTaskByTarget(
1114- bug_two, debian_firefox)
1115- bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
1116- bug_two_in_debian_firefox.transitionToStatus(
1117- BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
1118-
1119- flush_database_updates()
1120-
1121- def tearDownBugsElsewhereTests(self):
1122- """Resets the modified bugtasks to their original statuses."""
1123- self.firefox_upstream.transitionToStatus(
1124- self.old_firefox_status,
1125- self.firefox_upstream.target.bug_supervisor)
1126- self.thunderbird_upstream.transitionToStatus(
1127- self.old_thunderbird_status,
1128- self.firefox_upstream.target.bug_supervisor)
1129- flush_database_updates()
1130-
1131- def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
1132- """Assert the bugtask is pending a bug watch elsewhere.
1133-
1134- Pending a bugwatch elsewhere means that at least one of the bugtask's
1135- related task's target isn't using Malone, and that
1136- related_bugtask.bugwatch is None.
1137- """
1138- non_malone_using_bugtasks = [
1139- related_task for related_task in bugtask.related_tasks
1140- if not related_task.target_uses_malone]
1141- pending_bugwatch_bugtasks = [
1142- related_bugtask for related_bugtask in non_malone_using_bugtasks
1143- if related_bugtask.bugwatch is None]
1144- self.assert_(
1145- len(pending_bugwatch_bugtasks) > 0,
1146- 'Bugtask %s on %s has no related bug watches elsewhere.' % (
1147- bugtask.id, bugtask.target.displayname))
1148-
1149- def assertBugTaskIsResolvedUpstream(self, bugtask):
1150- """Make sure at least one of the related upstream tasks is resolved.
1151-
1152- "Resolved", for our purposes, means either that one of the related
1153- tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
1154- it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
1155- INVALID state.
1156- """
1157- resolved_upstream_states = [
1158- BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
1159- resolved_bugwatch_states = [
1160- BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
1161- BugTaskStatus.INVALID]
1162-
1163- # Helper functions for the list comprehension below.
1164- def _is_resolved_upstream_task(bugtask):
1165- return (
1166- IUpstreamBugTask.providedBy(bugtask) and
1167- bugtask.status in resolved_upstream_states)
1168-
1169- def _is_resolved_bugwatch_task(bugtask):
1170- return (
1171- bugtask.bugwatch and bugtask.status in
1172- resolved_bugwatch_states)
1173-
1174- resolved_related_tasks = [
1175- related_task for related_task in bugtask.related_tasks
1176- if (_is_resolved_upstream_task(related_task) or
1177- _is_resolved_bugwatch_task(related_task))]
1178-
1179- self.assert_(len(resolved_related_tasks) > 0)
1180- self.assert_(
1181- len(resolved_related_tasks) > 0,
1182- 'Bugtask %s on %s has no resolved related tasks.' % (
1183- bugtask.id, bugtask.target.displayname))
1184-
1185- def assertBugTaskIsOpenUpstream(self, bugtask):
1186- """Make sure at least one of the related upstream tasks is open.
1187-
1188- "Open", for our purposes, means either that one of the related
1189- tasks is an upstream task or a task with a bugwatch which has
1190- one of the states listed in open_states.
1191- """
1192- open_states = [
1193- BugTaskStatus.NEW,
1194- BugTaskStatus.INCOMPLETE,
1195- BugTaskStatus.CONFIRMED,
1196- BugTaskStatus.INPROGRESS,
1197- BugTaskStatus.UNKNOWN]
1198-
1199- # Helper functions for the list comprehension below.
1200- def _is_open_upstream_task(bugtask):
1201- return (
1202- IUpstreamBugTask.providedBy(bugtask) and
1203- bugtask.status in open_states)
1204-
1205- def _is_open_bugwatch_task(bugtask):
1206- return (
1207- bugtask.bugwatch and bugtask.status in
1208- open_states)
1209-
1210- open_related_tasks = [
1211- related_task for related_task in bugtask.related_tasks
1212- if (_is_open_upstream_task(related_task) or
1213- _is_open_bugwatch_task(related_task))]
1214-
1215- self.assert_(
1216- len(open_related_tasks) > 0,
1217- 'Bugtask %s on %s has no open related tasks.' % (
1218- bugtask.id, bugtask.target.displayname))
1219-
1220- def _hasUpstreamTask(self, bug):
1221- """Does this bug have an upstream task associated with it?
1222-
1223- Returns True if yes, otherwise False.
1224- """
1225- for bugtask in bug.bugtasks:
1226- if IUpstreamBugTask.providedBy(bugtask):
1227- return True
1228- return False
1229-
1230- def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
1231- """Should the bugtask be shown in the search no upstream task search?
1232-
1233- Returns True if yes, otherwise False.
1234- """
1235- self.assert_(
1236- not self._hasUpstreamTask(bugtask.bug),
1237- 'Bugtask %s on %s has upstream tasks.' % (
1238- bugtask.id, bugtask.target.displayname))
1239-
1240-
1241-class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
1242- """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
1243- layer = DatabaseFunctionalLayer
1244-
1245- def setUp(self):
1246- """Setup the zope interaction and create expirable bugtasks."""
1247- login('test@canonical.com')
1248- self.user = getUtility(ILaunchBag).user
1249- self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
1250- self.distroseries = self.distribution.getSeries('hoary')
1251- self.product = getUtility(IProductSet).getByName('jokosher')
1252- self.productseries = self.product.getSeries('trunk')
1253- self.bugtaskset = getUtility(IBugTaskSet)
1254- bugtasks = []
1255- bugtasks.append(
1256- create_old_bug("90 days old", 90, self.distribution))
1257- bugtasks.append(
1258- self.bugtaskset.createTask(
1259- bug=bugtasks[-1].bug, owner=self.user,
1260- distroseries=self.distroseries))
1261- bugtasks.append(
1262- create_old_bug("90 days old", 90, self.product))
1263- bugtasks.append(
1264- self.bugtaskset.createTask(
1265- bug=bugtasks[-1].bug, owner=self.user,
1266- productseries=self.productseries))
1267- sync_bugtasks(bugtasks)
1268-
1269- def tearDown(self):
1270- logout()
1271-
1272- def testSupportedTargetParam(self):
1273- """The target param supports a limited set of BugTargets.
1274-
1275- Four BugTarget types may passed as the target argument:
1276- Distribution, DistroSeries, Product, ProductSeries.
1277- """
1278- supported_targets = [self.distribution, self.distroseries,
1279- self.product, self.productseries]
1280- for target in supported_targets:
1281- expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
1282- 0, self.user, target=target)
1283- self.assertNotEqual(expirable_bugtasks.count(), 0,
1284- "%s has %d expirable bugtasks." %
1285- (self.distroseries, expirable_bugtasks.count()))
1286-
1287- def testUnsupportedBugTargetParam(self):
1288- """Test that unsupported targets raise errors.
1289-
1290- Three BugTarget types are not supported because the UI does not
1291- provide bug-index to link to the 'bugs that can expire' page.
1292- ProjectGroup, SourcePackage, and DistributionSourcePackage will
1293- raise an NotImplementedError.
1294-
1295- Passing an unknown bugtarget type will raise an AssertionError.
1296- """
1297- project = getUtility(IProjectGroupSet).getByName('mozilla')
1298- distributionsourcepackage = self.distribution.getSourcePackage(
1299- 'mozilla-firefox')
1300- sourcepackage = self.distroseries.getSourcePackage(
1301- 'mozilla-firefox')
1302- unsupported_targets = [project, distributionsourcepackage,
1303- sourcepackage]
1304- for target in unsupported_targets:
1305- self.assertRaises(
1306- NotImplementedError, self.bugtaskset.findExpirableBugTasks,
1307- 0, self.user, target=target)
1308-
1309- # Objects that are not a known BugTarget type raise an AssertionError.
1310- self.assertRaises(
1311- AssertionError, self.bugtaskset.findExpirableBugTasks,
1312- 0, self.user, target=[])
1313-
1314-
1315-class BugTaskSetTest(unittest.TestCase):
1316- """Test `BugTaskSet` methods."""
1317- layer = DatabaseFunctionalLayer
1318-
1319- def setUp(self):
1320- login(ANONYMOUS)
1321-
1322- def test_getBugTasks(self):
1323- """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
1324- bugs to their bugtasks. It does that in a single query, to avoid
1325- hitting the DB again when getting the bugs' tasks.
1326- """
1327- login('no-priv@canonical.com')
1328- factory = LaunchpadObjectFactory()
1329- bug1 = factory.makeBug()
1330- factory.makeBugTask(bug1)
1331- bug2 = factory.makeBug()
1332- factory.makeBugTask(bug2)
1333- factory.makeBugTask(bug2)
1334-
1335- bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
1336- [bug1.id, bug2.id])
1337- # The bugtasks returned by getBugTasks() are exactly the same as the
1338- # ones returned by bug.bugtasks, obviously.
1339- self.failUnlessEqual(
1340- set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
1341- set([]))
1342- self.failUnlessEqual(
1343- set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
1344- set([]))
1345-
1346- def test_getBugTasks_with_empty_list(self):
1347- # When given an empty list of bug IDs, getBugTasks() will return an
1348- # empty dictionary.
1349- bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
1350- self.failUnlessEqual(bugs_and_tasks, {})
1351
1352=== modified file 'lib/lp/registry/doc/structural-subscriptions.txt'
1353--- lib/lp/registry/doc/structural-subscriptions.txt 2010-10-17 15:44:08 +0000
1354+++ lib/lp/registry/doc/structural-subscriptions.txt 2010-10-25 19:52:46 +0000
1355@@ -86,117 +86,6 @@
1356 When notifying subscribers of bug activity, both subscribers to the
1357 target and to the target's parent are notified.
1358
1359- >>> from canonical.launchpad.ftests import syncUpdate
1360- >>> from lp.registry.enum import BugNotificationLevel
1361- >>> from lp.registry.interfaces.structuralsubscription import BlueprintNotificationLevel
1362- >>> from lp.bugs.mail.bugnotificationrecipients import (
1363- ... BugNotificationRecipients)
1364-
1365-We define some utility functions for printing out bug subscriptions and
1366-the recipients for the notifications they generate.
1367-
1368- >>> def print_bug_subscribers(bug_subscribers):
1369- ... subscriber_names = sorted(subscriber.name
1370- ... for subscriber in bug_subscribers)
1371- ... for name in subscriber_names:
1372- ... print name
1373- >>> def print_bug_subscriptions(bug_subscriptions):
1374- ... for subscription in bug_subscriptions:
1375- ... print subscription.subscriber.name
1376- >>> def print_bug_recipients(recipients):
1377- ... for recipient in recipients:
1378- ... reason = recipients.getReason(recipient)
1379- ... print '%s "%s"' % (recipient.name, reason[1])
1380-
1381-Sample person has a subscription to Ubuntu and to the Evolution package
1382-in Ubuntu. We set the bug notification level for both subscriptions.
1383-
1384- >>> ubuntu_sub.bug_notification_level = BugNotificationLevel.COMMENTS
1385- >>> evolution_sub.bug_notification_level = BugNotificationLevel.COMMENTS
1386-
1387-`getBugNotificationsRecipients` returns all the bug subscribers to the
1388-target and its parent, and adds the rationale for the subscriptions to
1389-the recipients set. Each subscriber is only added once.
1390-
1391- >>> recipients = BugNotificationRecipients()
1392- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
1393- ... recipients=recipients)
1394- >>> print_bug_subscriptions(ubuntu.bug_subscriptions)
1395- name12
1396- >>> print_bug_subscriptions(evolution_package.bug_subscriptions)
1397- name12
1398- >>> print_bug_subscribers(bug_subscribers)
1399- name12
1400- >>> print_bug_recipients(recipients)
1401- name12 "Subscriber (evolution in ubuntu)"
1402-
1403-Foo Bar subscribes to Ubuntu.
1404-
1405- >>> login('foo.bar@canonical.com')
1406- >>> foobar_subscription = ubuntu.addBugSubscription(foobar, foobar)
1407- >>> recipients = BugNotificationRecipients()
1408-
1409-The set of subscribers to the evolution package for ubuntu now includes
1410-both subscribers to the package, and subscribers to the distribution.
1411-
1412- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
1413- ... recipients=recipients)
1414- >>> print_bug_recipients(recipients)
1415- name16 "Subscriber (Ubuntu)"
1416- name12 "Subscriber (evolution in ubuntu)"
1417-
1418-We can pass the parameter `level` to getBugNotificationsRecipients().
1419-Subscribers whose subscription level is lower than the given parameter
1420-are not returned.
1421-
1422- >>> foobar_subscription.bug_notification_level = (
1423- ... BugNotificationLevel.METADATA)
1424- >>> recipients = BugNotificationRecipients()
1425- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
1426- ... recipients=recipients, level=BugNotificationLevel.COMMENTS)
1427- >>> print_bug_recipients(recipients)
1428- name12 "Subscriber (evolution in ubuntu)"
1429-
1430-We remove Sample Person's bug subscription to the package.
1431-
1432- >>> evolution_sub.blueprint_notification_level = (
1433- ... BlueprintNotificationLevel.METADATA)
1434- >>> evolution_package.removeBugSubscription(sampleperson, sampleperson)
1435- >>> ubuntu.removeBugSubscription(sampleperson, sampleperson)
1436- >>> syncUpdate(evolution_sub)
1437-
1438-Sample Person is no longer a subscriber to the package, but Foo Bar
1439-is still a subscriber, by being subscribed to Ubuntu.
1440-
1441- >>> print_bug_subscribers(
1442- ... evolution_package.getBugNotificationsRecipients(
1443- ... recipients=recipients))
1444- name16
1445-
1446-A project is the parent of each of its products.
1447-
1448-Fireox does not have any subscribers.
1449-
1450- >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
1451-
1452-Mozilla is the parent of Fireox.
1453-
1454- >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
1455- >>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
1456- >>> print firefox.parent_subscription_target.displayname
1457- the Mozilla Project
1458-
1459-Foobar subscribes to bug notificatios for Mozilla.
1460-
1461- >>> mozilla.addBugSubscription(foobar, foobar)
1462- <StructuralSubscription at ...>
1463-
1464-As a result of subscribing to Mozilla, Foobar is now a subscriber of
1465-Firefox.
1466-
1467- >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
1468- name16
1469-
1470
1471 Target type display
1472 ===================
1473
1474=== modified file 'lib/lp/registry/interfaces/structuralsubscription.py'
1475--- lib/lp/registry/interfaces/structuralsubscription.py 2010-10-07 10:06:55 +0000
1476+++ lib/lp/registry/interfaces/structuralsubscription.py 2010-10-25 19:52:46 +0000
1477@@ -181,20 +181,6 @@
1478 def getSubscription(person):
1479 """Return the subscription for `person`, if it exists."""
1480
1481- def getBugNotificationsRecipients(recipients=None, level=None):
1482- """Return the set of bug subscribers to this target.
1483-
1484- :param recipients: If recipients is not None, a rationale
1485- is added for each subscriber.
1486- :type recipients: `INotificationRecipientSet`
1487- 'param level: If level is not None, only strucutral
1488- subscribers with a subscrition level greater or equal
1489- to the given value are returned.
1490- :type level: `BugNotificationLevel`
1491- :return: An `INotificationRecipientSet` instance containing
1492- the bug subscribers.
1493- """
1494-
1495 target_type_display = Attribute("The type of the target, for display.")
1496
1497 def userHasBugSubscriptions(user):
1498
1499=== modified file 'lib/lp/registry/model/structuralsubscription.py'
1500--- lib/lp/registry/model/structuralsubscription.py 2010-10-07 10:06:55 +0000
1501+++ lib/lp/registry/model/structuralsubscription.py 2010-10-25 19:52:46 +0000
1502@@ -448,24 +448,6 @@
1503 return StructuralSubscription.select(
1504 query, orderBy='Person.displayname', clauseTables=['Person'])
1505
1506- def getBugNotificationsRecipients(self, recipients=None, level=None):
1507- """See `IStructuralSubscriptionTarget`."""
1508- if level is None:
1509- subscriptions = self.bug_subscriptions
1510- else:
1511- subscriptions = self.getSubscriptions(
1512- min_bug_notification_level=level)
1513- subscribers = set(
1514- subscription.subscriber for subscription in subscriptions)
1515- if recipients is not None:
1516- for subscriber in subscribers:
1517- recipients.addStructuralSubscriber(subscriber, self)
1518- parent = self.parent_subscription_target
1519- if parent is not None:
1520- subscribers.update(
1521- parent.getBugNotificationsRecipients(recipients, level))
1522- return subscribers
1523-
1524 @property
1525 def bug_subscriptions(self):
1526 """See `IStructuralSubscriptionTarget`."""