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

Proposed by Gavin Panella
Status: Rejected
Rejected by: Gavin Panella
Proposed branch: lp:~allenap/launchpad/ditch-get-bug-notifications-recipients-bug-659085
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~allenap/launchpad/wire-up-filter-subs-bug-655567
Diff against target: 2497 lines (+1153/-1028)
19 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/-6)
lib/lp/bugs/interfaces/bugtask.py (+6/-0)
lib/lp/bugs/model/bug.py (+5/-59)
lib/lp/bugs/model/bugtask.py (+61/-1)
lib/lp/bugs/model/tests/test_bug.py (+0/-85)
lib/lp/bugs/model/tests/test_bugtask.py (+442/-2)
lib/lp/bugs/model/tests/test_bugtask_status.py (+356/-0)
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/bugs/tests/test_bugtask_status.py (+0/-356)
lib/lp/code/interfaces/branchmergequeue.py (+115/-0)
lib/lp/code/model/tests/test_branchmergequeue.py (+155/-0)
lib/lp/registry/doc/structural-subscriptions.txt (+0/-87)
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
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+38325@code.launchpad.net

Commit message

Remove getBugNotificationRecipients()

Description of the change

This is a big branch, but fear not, most of it is moving code and
deletions. I shall guide you though the dark forest...

- tests/test_bugtask_0.py, tests/test_bugtask_1.py and
  tests/test_bugtask_status.py have been folded into
  tests/test_bugtask.py. The first two were left over from the
  automated tree migration.

- test_bugtask_0.py contained one doctest-in-a-doctring which was
  rewritten as a unittest (it's *very* short). It's still called
  test_open_and_resolved_statuses.

- tests/test_bugtask.py and tests/test_bugtask_status.txt have moved
  into model/tests.

- Bug.getStructuralSubscribers() has moved to BugTaskSet. It now
  accepts a list of bugtasks instead of getting them from
  self.bugtasks, and gets the store to use with IStore(Person) rather
  than Store.of(self).

- TestBugStructuralSubscribers has moved from model/tests/test_bug.py
  to model/tests/test_bugtask.py, and is now called
  TestGetStructuralSubscribers.

- A couple of functions have been changed to use
  BugTaskSet.getStructuralSubscribers() via the utility.

- getBugNotificationRecipients() has been removed, along with its
  associated tests.

I'm sorry it got a bit long. The dependencies of each change cascaded
somewhat.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
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-22 18:48:58 +0000
3+++ lib/lp/bugs/configure.zcml 2010-10-25 11:13:20 +0000
4@@ -708,7 +708,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 11:13:20 +0000
16@@ -299,7 +299,7 @@
17 ... BugTaskImportance,
18 ... BugTaskStatus,
19 ... )
20- >>> from lp.bugs.tests.test_bugtask_1 import (
21+ >>> from lp.bugs.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-16 14:20:57 +0000
28+++ lib/lp/bugs/interfaces/bug.py 2010-10-25 11:13:20 +0000
29@@ -493,12 +493,6 @@
30 from duplicates.
31 """
32
33- def getStructuralSubscribers(recipients=None, level=None):
34- """Return `IPerson`s subscribed to this bug's targets.
35-
36- This takes into account bug subscription filters.
37- """
38-
39 def getSubscriptionsFromDuplicates():
40 """Return IBugSubscriptions subscribed from dupes of this bug."""
41
42
43=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
44--- lib/lp/bugs/interfaces/bugtask.py 2010-10-22 21:38:42 +0000
45+++ lib/lp/bugs/interfaces/bugtask.py 2010-10-25 11:13:20 +0000
46@@ -1524,6 +1524,12 @@
47 def getOpenBugTasksPerProduct(user, products):
48 """Return open bugtask count for multiple products."""
49
50+ def getStructuralSubscribers(bugtasks, recipients=None, level=None):
51+ """Return `IPerson`s subscribed to the given bug tasks.
52+
53+ This takes into account bug subscription filters.
54+ """
55+
56
57 def valid_remote_bug_url(value):
58 """Verify that the URL is to a bug to a known bug tracker."""
59
60=== modified file 'lib/lp/bugs/model/bug.py'
61--- lib/lp/bugs/model/bug.py 2010-10-22 13:43:50 +0000
62+++ lib/lp/bugs/model/bug.py 2010-10-25 11:13:20 +0000
63@@ -176,9 +176,6 @@
64 from lp.registry.interfaces.productseries import IProductSeries
65 from lp.registry.interfaces.series import SeriesStatus
66 from lp.registry.interfaces.sourcepackage import ISourcePackage
67-from lp.registry.interfaces.structuralsubscription import (
68- IStructuralSubscriptionTarget,
69- )
70 from lp.registry.model.mentoringoffer import MentoringOffer
71 from lp.registry.model.person import (
72 Person,
73@@ -934,8 +931,9 @@
74 # XXX: RobertCollins 2010-09-22 bug=374777: This SQL(...) is a
75 # hack; it does not seem to be possible to express DISTINCT ON
76 # with Storm.
77- (SQL("DISTINCT ON (Person.name, BugSubscription.person) 0 AS ignore"),
78- # return people and subscribptions
79+ (SQL("DISTINCT ON (Person.name, BugSubscription.person) "
80+ "0 AS ignore"),
81+ # Return people and subscriptions
82 Person, BugSubscription),
83 # For this bug or its duplicates
84 Or(
85@@ -986,8 +984,8 @@
86
87 # Structural subscribers.
88 also_notified_subscribers.update(
89- self.getStructuralSubscribers(
90- recipients=recipients, level=level))
91+ getUtility(IBugTaskSet).getStructuralSubscribers(
92+ self.bugtasks, recipients=recipients, level=level))
93
94 # Direct subscriptions always take precedence over indirect
95 # subscriptions.
96@@ -999,58 +997,6 @@
97 (also_notified_subscribers - direct_subscribers),
98 key=lambda x: removeSecurityProxy(x).displayname)
99
100- def getStructuralSubscribers(self, recipients=None, level=None):
101- """See `IBug`. """
102- query_arguments = []
103- for bugtask in self.bugtasks:
104- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
105- query_arguments.append((bugtask.target, bugtask))
106- if bugtask.target.parent_subscription_target is not None:
107- query_arguments.append(
108- (bugtask.target.parent_subscription_target, bugtask))
109- if ISourcePackage.providedBy(bugtask.target):
110- # Distribution series bug tasks with a package have the source
111- # package set as their target, so we add the distroseries
112- # explicitly to the set of subscription targets.
113- query_arguments.append((bugtask.distroseries, bugtask))
114- if bugtask.milestone is not None:
115- query_arguments.append((bugtask.milestone, bugtask))
116-
117- if len(query_arguments) == 0:
118- return EmptyResultSet()
119-
120- if level is None:
121- # If level is not specified, default to NOTHING so that all
122- # subscriptions are found. XXX: Perhaps this should go in
123- # getSubscriptionsForBugTask()?
124- level = BugNotificationLevel.NOTHING
125-
126- # Build the query.
127- union = lambda left, right: left.union(right)
128- queries = (
129- target.getSubscriptionsForBugTask(bugtask, level)
130- for target, bugtask in query_arguments)
131- subscriptions = reduce(union, queries)
132-
133- # Pull all the subscriptions in.
134- subscriptions = list(subscriptions)
135-
136- # Prepare a query for the subscribers.
137- subscribers = Store.of(self).find(
138- Person, Person.id.is_in(
139- subscription.subscriberID
140- for subscription in subscriptions))
141-
142- if recipients is not None:
143- # We need to process subscriptions, so pull all the subscribes
144- # into the cache, then update recipients with the subscriptions.
145- subscribers = list(subscribers)
146- for subscription in subscriptions:
147- recipients.addStructuralSubscriber(
148- subscription.subscriber, subscription.target)
149-
150- return subscribers
151-
152 def getBugNotificationRecipients(self, duplicateof=None, old_bug=None,
153 level=None,
154 include_master_dupe_subscribers=False):
155
156=== modified file 'lib/lp/bugs/model/bugtask.py'
157--- lib/lp/bugs/model/bugtask.py 2010-10-22 19:56:26 +0000
158+++ lib/lp/bugs/model/bugtask.py 2010-10-25 11:13:20 +0000
159@@ -127,6 +127,7 @@
160 )
161 from lp.bugs.model.bugnomination import BugNomination
162 from lp.bugs.model.bugsubscription import BugSubscription
163+from lp.registry.enum import BugNotificationLevel
164 from lp.registry.interfaces.distribution import (
165 IDistribution,
166 IDistributionSet,
167@@ -155,6 +156,9 @@
168 from lp.registry.interfaces.projectgroup import IProjectGroup
169 from lp.registry.interfaces.sourcepackage import ISourcePackage
170 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
171+from lp.registry.interfaces.structuralsubscription import (
172+ IStructuralSubscriptionTarget,
173+ )
174 from lp.registry.model.pillar import pillar_sort_key
175 from lp.registry.model.sourcepackagename import SourcePackageName
176 from lp.services.propertycache import IPropertyCache
177@@ -1953,7 +1957,7 @@
178 query = " AND ".join(extra_clauses)
179
180 if not decorators:
181- decorator = lambda x:x
182+ decorator = lambda x: x
183 else:
184 def decorator(obj):
185 for decor in decorators:
186@@ -2835,3 +2839,59 @@
187 counts.append(package_counts)
188
189 return counts
190+
191+ def getStructuralSubscribers(self, bugtasks, recipients=None, level=None):
192+ """See `IBugTaskSet`."""
193+ # getStructuralSubscribers() is called from various sites, but needs
194+ # bugtasks without security proxies.
195+ bugtasks = [removeSecurityProxy(bugtask) for bugtask in bugtasks]
196+
197+ query_arguments = []
198+ for bugtask in bugtasks:
199+ if IStructuralSubscriptionTarget.providedBy(bugtask.target):
200+ query_arguments.append((bugtask.target, bugtask))
201+ if bugtask.target.parent_subscription_target is not None:
202+ query_arguments.append(
203+ (bugtask.target.parent_subscription_target, bugtask))
204+ if ISourcePackage.providedBy(bugtask.target):
205+ # Distribution series bug tasks with a package have the source
206+ # package set as their target, so we add the distroseries
207+ # explicitly to the set of subscription targets.
208+ query_arguments.append((bugtask.distroseries, bugtask))
209+ if bugtask.milestone is not None:
210+ query_arguments.append((bugtask.milestone, bugtask))
211+
212+ if len(query_arguments) == 0:
213+ return EmptyResultSet()
214+
215+ if level is None:
216+ # If level is not specified, default to NOTHING so that all
217+ # subscriptions are found.
218+ level = BugNotificationLevel.NOTHING
219+
220+ # Build the query.
221+ union = lambda left, right: left.union(right)
222+ queries = (
223+ target.getSubscriptionsForBugTask(bugtask, level)
224+ for target, bugtask in query_arguments)
225+ subscriptions = reduce(union, queries)
226+
227+ # Pull all the subscriptions in.
228+ subscriptions = list(subscriptions)
229+
230+ # Prepare a query for the subscribers.
231+ from lp.registry.model.person import Person
232+ subscribers = IStore(Person).find(
233+ Person, Person.id.is_in(
234+ subscription.subscriberID
235+ for subscription in subscriptions))
236+
237+ if recipients is not None:
238+ # We need to process subscriptions, so pull all the subscribes into
239+ # the cache, then update recipients with the subscriptions.
240+ subscribers = list(subscribers)
241+ for subscription in subscriptions:
242+ recipients.addStructuralSubscriber(
243+ subscription.subscriber, subscription.target)
244+
245+ return subscribers
246
247=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
248--- lib/lp/bugs/model/tests/test_bug.py 2010-10-16 14:20:57 +0000
249+++ lib/lp/bugs/model/tests/test_bug.py 2010-10-25 11:13:20 +0000
250@@ -5,10 +5,7 @@
251
252 __metaclass__ = type
253
254-from storm.store import ResultSet
255-
256 from canonical.testing.layers import DatabaseFunctionalLayer
257-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
258 from lp.registry.enum import BugNotificationLevel
259 from lp.registry.interfaces.person import PersonVisibility
260 from lp.registry.model.structuralsubscription import StructuralSubscription
261@@ -17,7 +14,6 @@
262 person_logged_in,
263 TestCaseWithFactory,
264 )
265-from lp.testing.matchers import StartsWith
266
267
268 class TestBug(TestCaseWithFactory):
269@@ -246,84 +242,3 @@
270 self.assertTrue(
271 subscriber not in duplicate_subscribers,
272 "Subscriber should not be in duplicate_subscribers.")
273-
274-
275-class TestBugStructuralSubscribers(TestCaseWithFactory):
276-
277- layer = DatabaseFunctionalLayer
278-
279- def test_getStructuralSubscribers_no_subscribers(self):
280- # If there are no subscribers for any of the bug's targets then no
281- # subscribers will be returned by getStructuralSubscribers().
282- product = self.factory.makeProduct()
283- bug = self.factory.makeBug(product=product)
284- subscribers = bug.getStructuralSubscribers()
285- self.assertIsInstance(subscribers, ResultSet)
286- self.assertEqual([], list(subscribers))
287-
288- def test_getStructuralSubscribers_single_target(self):
289- # Subscribers for any of the bug's targets are returned.
290- subscriber = self.factory.makePerson()
291- login_person(subscriber)
292- product = self.factory.makeProduct()
293- product.addBugSubscription(subscriber, subscriber)
294- bug = self.factory.makeBug(product=product)
295- self.assertEqual([subscriber], list(bug.getStructuralSubscribers()))
296-
297- def test_getStructuralSubscribers_multiple_targets(self):
298- # Subscribers for any of the bug's targets are returned.
299- actor = self.factory.makePerson()
300- login_person(actor)
301-
302- subscriber1 = self.factory.makePerson()
303- subscriber2 = self.factory.makePerson()
304-
305- product1 = self.factory.makeProduct(owner=actor)
306- product1.addBugSubscription(subscriber1, subscriber1)
307- product2 = self.factory.makeProduct(owner=actor)
308- product2.addBugSubscription(subscriber2, subscriber2)
309-
310- bug = self.factory.makeBug(product=product1)
311- bug.addTask(actor, product2)
312-
313- subscribers = bug.getStructuralSubscribers()
314- self.assertIsInstance(subscribers, ResultSet)
315- self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
316-
317- def test_getStructuralSubscribers_recipients(self):
318- # If provided, getStructuralSubscribers() calls the appropriate
319- # methods on a BugNotificationRecipients object.
320- subscriber = self.factory.makePerson()
321- login_person(subscriber)
322- product = self.factory.makeProduct()
323- product.addBugSubscription(subscriber, subscriber)
324- bug = self.factory.makeBug(product=product)
325- recipients = BugNotificationRecipients()
326- subscribers = bug.getStructuralSubscribers(recipients=recipients)
327- # The return value is a list only when populating recipients.
328- self.assertIsInstance(subscribers, list)
329- self.assertEqual([subscriber], recipients.getRecipients())
330- reason, header = recipients.getReason(subscriber)
331- self.assertThat(
332- reason, StartsWith(
333- u"You received this bug notification because "
334- u"you are subscribed to "))
335- self.assertThat(header, StartsWith(u"Subscriber "))
336-
337- def test_getStructuralSubscribers_level(self):
338- # getStructuralSubscribers() respects the given level.
339- subscriber = self.factory.makePerson()
340- login_person(subscriber)
341- product = self.factory.makeProduct()
342- subscription = product.addBugSubscription(subscriber, subscriber)
343- subscription.bug_notification_level = BugNotificationLevel.METADATA
344- bug = self.factory.makeBug(product=product)
345- self.assertEqual(
346- [subscriber], list(
347- bug.getStructuralSubscribers(
348- level=BugNotificationLevel.METADATA)))
349- subscription.bug_notification_level = BugNotificationLevel.METADATA
350- self.assertEqual(
351- [], list(
352- bug.getStructuralSubscribers(
353- level=BugNotificationLevel.COMMENTS)))
354
355=== renamed file 'lib/lp/bugs/tests/test_bugtask.py' => 'lib/lp/bugs/model/tests/test_bugtask.py'
356--- lib/lp/bugs/tests/test_bugtask.py 2010-10-21 16:40:10 +0000
357+++ lib/lp/bugs/model/tests/test_bugtask.py 2010-10-25 11:13:20 +0000
358@@ -8,31 +8,52 @@
359 import unittest
360
361 from lazr.lifecycle.snapshot import Snapshot
362+from storm.store import ResultSet
363 from zope.component import getUtility
364 from zope.interface import providedBy
365
366+from canonical.database.sqlbase import flush_database_updates
367 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
368 from canonical.launchpad.searchbuilder import (
369 all,
370 any,
371 )
372+from canonical.launchpad.webapp.interfaces import ILaunchBag
373 from canonical.testing.layers import (
374 DatabaseFunctionalLayer,
375 LaunchpadZopelessLayer,
376 )
377+from lp.app.enums import ServiceUsage
378+from lp.bugs.interfaces.bug import IBugSet
379 from lp.bugs.interfaces.bugtarget import IBugTarget
380 from lp.bugs.interfaces.bugtask import (
381 BugTaskImportance,
382 BugTaskSearchParams,
383 BugTaskStatus,
384+ IBugTaskSet,
385+ IUpstreamBugTask,
386+ RESOLVED_BUGTASK_STATUSES,
387+ UNRESOLVED_BUGTASK_STATUSES,
388 )
389+from lp.bugs.interfaces.bugwatch import IBugWatchSet
390+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
391 from lp.bugs.model.bugtask import build_tag_search_clause
392+from lp.bugs.tests.bug import (
393+ create_old_bug,
394+ sync_bugtasks,
395+ )
396 from lp.hardwaredb.interfaces.hwdb import (
397 HWBus,
398 IHWDeviceSet,
399 )
400+from lp.registry.enum import BugNotificationLevel
401 from lp.registry.interfaces.distribution import IDistributionSet
402-from lp.registry.interfaces.person import IPerson, IPersonSet
403+from lp.registry.interfaces.person import (
404+ IPerson,
405+ IPersonSet,
406+ )
407+from lp.registry.interfaces.product import IProductSet
408+from lp.registry.interfaces.projectgroup import IProjectGroupSet
409 from lp.testing import (
410 ANONYMOUS,
411 login,
412@@ -42,6 +63,11 @@
413 TestCase,
414 TestCaseWithFactory,
415 )
416+from lp.testing.factory import (
417+ is_security_proxied_or_harmless,
418+ LaunchpadObjectFactory,
419+ )
420+from lp.testing.matchers import StartsWith
421
422
423 class TestBugTaskDelta(TestCaseWithFactory):
424@@ -892,7 +918,7 @@
425 self.assertEqual(2, tasks.count())
426 # Cache in the storm cache the account->person lookup so its not
427 # distorting what we're testing.
428- _ = IPerson(person.account, None)
429+ IPerson(person.account, None)
430 # One query and only one should be issued to get the tasks, bugs and
431 # allow access to getConjoinedMaster attribute - an attribute that
432 # triggers a permission check (nb: id does not trigger such a check)
433@@ -945,6 +971,420 @@
434 self.assertEqual([task2], list(result))
435
436
437+class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
438+ """Tests for searching bugs filtering on related bug tasks.
439+
440+ It also acts as a helper class, which makes related doctests more
441+ readable, since they can use methods from this class.
442+ """
443+ layer = DatabaseFunctionalLayer
444+
445+ def __init__(self, methodName='runTest', helper_only=False):
446+ """If helper_only is True, set up it only as a helper class."""
447+ if not helper_only:
448+ unittest.TestCase.__init__(self, methodName=methodName)
449+
450+ def setUp(self):
451+ login(ANONYMOUS)
452+
453+ def tearDown(self):
454+ logout()
455+
456+ def _getBugTaskByTarget(self, bug, target):
457+ """Return a bug's bugtask for the given target."""
458+ for bugtask in bug.bugtasks:
459+ if bugtask.target == target:
460+ return bugtask
461+ else:
462+ raise AssertionError(
463+ "Didn't find a %s task on bug %s." % (
464+ target.bugtargetname, bug.id))
465+
466+ def setUpBugsResolvedUpstreamTests(self):
467+ """Modify some bugtasks to match the resolved upstream filter."""
468+ bugset = getUtility(IBugSet)
469+ productset = getUtility(IProductSet)
470+ firefox = productset.getByName("firefox")
471+ thunderbird = productset.getByName("thunderbird")
472+
473+ # Mark an upstream task on bug #1 "Fix Released"
474+ bug_one = bugset.get(1)
475+ firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
476+ self.assertEqual(
477+ ServiceUsage.LAUNCHPAD,
478+ firefox_upstream.product.bug_tracking_usage)
479+ self.old_firefox_status = firefox_upstream.status
480+ firefox_upstream.transitionToStatus(
481+ BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
482+ self.firefox_upstream = firefox_upstream
483+
484+ # Mark an upstream task on bug #9 "Fix Committed"
485+ bug_nine = bugset.get(9)
486+ thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
487+ self.old_thunderbird_status = thunderbird_upstream.status
488+ thunderbird_upstream.transitionToStatus(
489+ BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
490+ self.thunderbird_upstream = thunderbird_upstream
491+
492+ # Add a watch to a Debian bug for bug #2, and mark the task Fix
493+ # Released.
494+ bug_two = bugset.get(2)
495+ bugwatchset = getUtility(IBugWatchSet)
496+
497+ # Get a debbugs watch.
498+ watch_debbugs_327452 = bugwatchset.get(9)
499+ self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
500+ self.assertEquals(watch_debbugs_327452.remotebug, "327452")
501+
502+ # Associate the watch to a Fix Released task.
503+ debian = getUtility(IDistributionSet).getByName("debian")
504+ debian_firefox = debian.getSourcePackage("mozilla-firefox")
505+ bug_two_in_debian_firefox = self._getBugTaskByTarget(
506+ bug_two, debian_firefox)
507+ bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
508+ bug_two_in_debian_firefox.transitionToStatus(
509+ BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
510+
511+ flush_database_updates()
512+
513+ def tearDownBugsElsewhereTests(self):
514+ """Resets the modified bugtasks to their original statuses."""
515+ self.firefox_upstream.transitionToStatus(
516+ self.old_firefox_status,
517+ self.firefox_upstream.target.bug_supervisor)
518+ self.thunderbird_upstream.transitionToStatus(
519+ self.old_thunderbird_status,
520+ self.firefox_upstream.target.bug_supervisor)
521+ flush_database_updates()
522+
523+ def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
524+ """Assert the bugtask is pending a bug watch elsewhere.
525+
526+ Pending a bugwatch elsewhere means that at least one of the bugtask's
527+ related task's target isn't using Malone, and that
528+ related_bugtask.bugwatch is None.
529+ """
530+ non_malone_using_bugtasks = [
531+ related_task for related_task in bugtask.related_tasks
532+ if not related_task.target_uses_malone]
533+ pending_bugwatch_bugtasks = [
534+ related_bugtask for related_bugtask in non_malone_using_bugtasks
535+ if related_bugtask.bugwatch is None]
536+ self.assert_(
537+ len(pending_bugwatch_bugtasks) > 0,
538+ 'Bugtask %s on %s has no related bug watches elsewhere.' % (
539+ bugtask.id, bugtask.target.displayname))
540+
541+ def assertBugTaskIsResolvedUpstream(self, bugtask):
542+ """Make sure at least one of the related upstream tasks is resolved.
543+
544+ "Resolved", for our purposes, means either that one of the related
545+ tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
546+ it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
547+ INVALID state.
548+ """
549+ resolved_upstream_states = [
550+ BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
551+ resolved_bugwatch_states = [
552+ BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
553+ BugTaskStatus.INVALID]
554+
555+ # Helper functions for the list comprehension below.
556+ def _is_resolved_upstream_task(bugtask):
557+ return (
558+ IUpstreamBugTask.providedBy(bugtask) and
559+ bugtask.status in resolved_upstream_states)
560+
561+ def _is_resolved_bugwatch_task(bugtask):
562+ return (
563+ bugtask.bugwatch and bugtask.status in
564+ resolved_bugwatch_states)
565+
566+ resolved_related_tasks = [
567+ related_task for related_task in bugtask.related_tasks
568+ if (_is_resolved_upstream_task(related_task) or
569+ _is_resolved_bugwatch_task(related_task))]
570+
571+ self.assert_(len(resolved_related_tasks) > 0)
572+ self.assert_(
573+ len(resolved_related_tasks) > 0,
574+ 'Bugtask %s on %s has no resolved related tasks.' % (
575+ bugtask.id, bugtask.target.displayname))
576+
577+ def assertBugTaskIsOpenUpstream(self, bugtask):
578+ """Make sure at least one of the related upstream tasks is open.
579+
580+ "Open", for our purposes, means either that one of the related
581+ tasks is an upstream task or a task with a bugwatch which has
582+ one of the states listed in open_states.
583+ """
584+ open_states = [
585+ BugTaskStatus.NEW,
586+ BugTaskStatus.INCOMPLETE,
587+ BugTaskStatus.CONFIRMED,
588+ BugTaskStatus.INPROGRESS,
589+ BugTaskStatus.UNKNOWN]
590+
591+ # Helper functions for the list comprehension below.
592+ def _is_open_upstream_task(bugtask):
593+ return (
594+ IUpstreamBugTask.providedBy(bugtask) and
595+ bugtask.status in open_states)
596+
597+ def _is_open_bugwatch_task(bugtask):
598+ return (
599+ bugtask.bugwatch and bugtask.status in
600+ open_states)
601+
602+ open_related_tasks = [
603+ related_task for related_task in bugtask.related_tasks
604+ if (_is_open_upstream_task(related_task) or
605+ _is_open_bugwatch_task(related_task))]
606+
607+ self.assert_(
608+ len(open_related_tasks) > 0,
609+ 'Bugtask %s on %s has no open related tasks.' % (
610+ bugtask.id, bugtask.target.displayname))
611+
612+ def _hasUpstreamTask(self, bug):
613+ """Does this bug have an upstream task associated with it?
614+
615+ Returns True if yes, otherwise False.
616+ """
617+ for bugtask in bug.bugtasks:
618+ if IUpstreamBugTask.providedBy(bugtask):
619+ return True
620+ return False
621+
622+ def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
623+ """Should the bugtask be shown in the search no upstream task search?
624+
625+ Returns True if yes, otherwise False.
626+ """
627+ self.assert_(
628+ not self._hasUpstreamTask(bugtask.bug),
629+ 'Bugtask %s on %s has upstream tasks.' % (
630+ bugtask.id, bugtask.target.displayname))
631+
632+
633+class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
634+ """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
635+ layer = DatabaseFunctionalLayer
636+
637+ def setUp(self):
638+ """Setup the zope interaction and create expirable bugtasks."""
639+ login('test@canonical.com')
640+ self.user = getUtility(ILaunchBag).user
641+ self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
642+ self.distroseries = self.distribution.getSeries('hoary')
643+ self.product = getUtility(IProductSet).getByName('jokosher')
644+ self.productseries = self.product.getSeries('trunk')
645+ self.bugtaskset = getUtility(IBugTaskSet)
646+ bugtasks = []
647+ bugtasks.append(
648+ create_old_bug("90 days old", 90, self.distribution))
649+ bugtasks.append(
650+ self.bugtaskset.createTask(
651+ bug=bugtasks[-1].bug, owner=self.user,
652+ distroseries=self.distroseries))
653+ bugtasks.append(
654+ create_old_bug("90 days old", 90, self.product))
655+ bugtasks.append(
656+ self.bugtaskset.createTask(
657+ bug=bugtasks[-1].bug, owner=self.user,
658+ productseries=self.productseries))
659+ sync_bugtasks(bugtasks)
660+
661+ def tearDown(self):
662+ logout()
663+
664+ def testSupportedTargetParam(self):
665+ """The target param supports a limited set of BugTargets.
666+
667+ Four BugTarget types may passed as the target argument:
668+ Distribution, DistroSeries, Product, ProductSeries.
669+ """
670+ supported_targets = [self.distribution, self.distroseries,
671+ self.product, self.productseries]
672+ for target in supported_targets:
673+ expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
674+ 0, self.user, target=target)
675+ self.assertNotEqual(expirable_bugtasks.count(), 0,
676+ "%s has %d expirable bugtasks." %
677+ (self.distroseries, expirable_bugtasks.count()))
678+
679+ def testUnsupportedBugTargetParam(self):
680+ """Test that unsupported targets raise errors.
681+
682+ Three BugTarget types are not supported because the UI does not
683+ provide bug-index to link to the 'bugs that can expire' page.
684+ ProjectGroup, SourcePackage, and DistributionSourcePackage will
685+ raise an NotImplementedError.
686+
687+ Passing an unknown bugtarget type will raise an AssertionError.
688+ """
689+ project = getUtility(IProjectGroupSet).getByName('mozilla')
690+ distributionsourcepackage = self.distribution.getSourcePackage(
691+ 'mozilla-firefox')
692+ sourcepackage = self.distroseries.getSourcePackage(
693+ 'mozilla-firefox')
694+ unsupported_targets = [project, distributionsourcepackage,
695+ sourcepackage]
696+ for target in unsupported_targets:
697+ self.assertRaises(
698+ NotImplementedError, self.bugtaskset.findExpirableBugTasks,
699+ 0, self.user, target=target)
700+
701+ # Objects that are not a known BugTarget type raise an AssertionError.
702+ self.assertRaises(
703+ AssertionError, self.bugtaskset.findExpirableBugTasks,
704+ 0, self.user, target=[])
705+
706+
707+class BugTaskSetTest(unittest.TestCase):
708+ """Test `BugTaskSet` methods."""
709+ layer = DatabaseFunctionalLayer
710+
711+ def setUp(self):
712+ login(ANONYMOUS)
713+
714+ def test_getBugTasks(self):
715+ """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
716+ bugs to their bugtasks. It does that in a single query, to avoid
717+ hitting the DB again when getting the bugs' tasks.
718+ """
719+ login('no-priv@canonical.com')
720+ factory = LaunchpadObjectFactory()
721+ bug1 = factory.makeBug()
722+ factory.makeBugTask(bug1)
723+ bug2 = factory.makeBug()
724+ factory.makeBugTask(bug2)
725+ factory.makeBugTask(bug2)
726+
727+ bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
728+ [bug1.id, bug2.id])
729+ # The bugtasks returned by getBugTasks() are exactly the same as the
730+ # ones returned by bug.bugtasks, obviously.
731+ self.failUnlessEqual(
732+ set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
733+ set([]))
734+ self.failUnlessEqual(
735+ set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
736+ set([]))
737+
738+ def test_getBugTasks_with_empty_list(self):
739+ # When given an empty list of bug IDs, getBugTasks() will return an
740+ # empty dictionary.
741+ bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
742+ self.failUnlessEqual(bugs_and_tasks, {})
743+
744+
745+class TestBugTaskStatuses(TestCase):
746+
747+ def test_open_and_resolved_statuses(self):
748+ """
749+ There are constants that are used to define which statuses are for
750+ resolved bugs (`RESOLVED_BUGTASK_STATUSES`), and which are for
751+ unresolved bugs (`UNRESOLVED_BUGTASK_STATUSES`). The two constants
752+ include all statuses defined in BugTaskStatus, except for Unknown.
753+ """
754+ self.assertNotIn(BugTaskStatus.UNKNOWN, RESOLVED_BUGTASK_STATUSES)
755+ self.assertNotIn(BugTaskStatus.UNKNOWN, UNRESOLVED_BUGTASK_STATUSES)
756+
757+
758+class TestGetStructuralSubscribers(TestCaseWithFactory):
759+
760+ layer = DatabaseFunctionalLayer
761+
762+ def make_product_with_bug(self):
763+ product = self.factory.makeProduct()
764+ bug = self.factory.makeBug(product=product)
765+ return product, bug
766+
767+ def getStructuralSubscribers(self, bugtasks, *args, **kwargs):
768+ # Call IBugTaskSet.getStructuralSubscribers() and check that the
769+ # result is security proxied.
770+ result = getUtility(IBugTaskSet).getStructuralSubscribers(
771+ bugtasks, *args, **kwargs)
772+ self.assertTrue(is_security_proxied_or_harmless(result))
773+ return result
774+
775+ def test_getStructuralSubscribers_no_subscribers(self):
776+ # If there are no subscribers for any of the bug's targets then no
777+ # subscribers will be returned by getStructuralSubscribers().
778+ product, bug = self.make_product_with_bug()
779+ subscribers = self.getStructuralSubscribers(bug.bugtasks)
780+ self.assertIsInstance(subscribers, ResultSet)
781+ self.assertEqual([], list(subscribers))
782+
783+ def test_getStructuralSubscribers_single_target(self):
784+ # Subscribers for any of the bug's targets are returned.
785+ subscriber = self.factory.makePerson()
786+ login_person(subscriber)
787+ product, bug = self.make_product_with_bug()
788+ product.addBugSubscription(subscriber, subscriber)
789+ self.assertEqual(
790+ [subscriber], list(
791+ self.getStructuralSubscribers(bug.bugtasks)))
792+
793+ def test_getStructuralSubscribers_multiple_targets(self):
794+ # Subscribers for any of the bug's targets are returned.
795+ actor = self.factory.makePerson()
796+ login_person(actor)
797+
798+ subscriber1 = self.factory.makePerson()
799+ subscriber2 = self.factory.makePerson()
800+
801+ product1 = self.factory.makeProduct(owner=actor)
802+ product1.addBugSubscription(subscriber1, subscriber1)
803+ product2 = self.factory.makeProduct(owner=actor)
804+ product2.addBugSubscription(subscriber2, subscriber2)
805+
806+ bug = self.factory.makeBug(product=product1)
807+ bug.addTask(actor, product2)
808+
809+ subscribers = self.getStructuralSubscribers(bug.bugtasks)
810+ self.assertIsInstance(subscribers, ResultSet)
811+ self.assertEqual(set([subscriber1, subscriber2]), set(subscribers))
812+
813+ def test_getStructuralSubscribers_recipients(self):
814+ # If provided, getStructuralSubscribers() calls the appropriate
815+ # methods on a BugNotificationRecipients object.
816+ subscriber = self.factory.makePerson()
817+ login_person(subscriber)
818+ product, bug = self.make_product_with_bug()
819+ product.addBugSubscription(subscriber, subscriber)
820+ recipients = BugNotificationRecipients()
821+ subscribers = self.getStructuralSubscribers(
822+ bug.bugtasks, recipients=recipients)
823+ # The return value is a list only when populating recipients.
824+ self.assertIsInstance(subscribers, list)
825+ self.assertEqual([subscriber], recipients.getRecipients())
826+ reason, header = recipients.getReason(subscriber)
827+ self.assertThat(
828+ reason, StartsWith(
829+ u"You received this bug notification because "
830+ u"you are subscribed to "))
831+ self.assertThat(header, StartsWith(u"Subscriber "))
832+
833+ def test_getStructuralSubscribers_level(self):
834+ # getStructuralSubscribers() respects the given level.
835+ subscriber = self.factory.makePerson()
836+ login_person(subscriber)
837+ product, bug = self.make_product_with_bug()
838+ subscription = product.addBugSubscription(subscriber, subscriber)
839+ subscription.bug_notification_level = BugNotificationLevel.METADATA
840+ self.assertEqual(
841+ [subscriber], list(
842+ self.getStructuralSubscribers(
843+ bug.bugtasks, level=BugNotificationLevel.METADATA)))
844+ subscription.bug_notification_level = BugNotificationLevel.METADATA
845+ self.assertEqual(
846+ [], list(
847+ self.getStructuralSubscribers(
848+ bug.bugtasks, level=BugNotificationLevel.COMMENTS)))
849+
850+
851 def test_suite():
852 suite = unittest.TestSuite()
853 suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
854
855=== added file 'lib/lp/bugs/model/tests/test_bugtask_status.py'
856--- lib/lp/bugs/model/tests/test_bugtask_status.py 1970-01-01 00:00:00 +0000
857+++ lib/lp/bugs/model/tests/test_bugtask_status.py 2010-10-25 11:13:20 +0000
858@@ -0,0 +1,356 @@
859+# Copyright 2009 Canonical Ltd. This software is licensed under the
860+# GNU Affero General Public License version 3 (see the file LICENSE).
861+
862+"""Tests for bug task status transitions."""
863+
864+__metaclass__ = type
865+
866+from zope.component import getUtility
867+from zope.security.proxy import removeSecurityProxy
868+
869+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
870+from canonical.testing.layers import LaunchpadFunctionalLayer
871+from lp.bugs.interfaces.bugtask import UserCannotEditBugTaskStatus
872+from lp.bugs.model.bugtask import BugTaskStatus
873+from lp.testing import (
874+ person_logged_in,
875+ TestCaseWithFactory,
876+ )
877+
878+
879+class TestBugTaskStatusTransitionForUser(TestCaseWithFactory):
880+ """Test bugtask status transitions for a regular logged in user."""
881+
882+ layer = LaunchpadFunctionalLayer
883+
884+ def setUp(self):
885+ super(TestBugTaskStatusTransitionForUser, self).setUp()
886+ self.user = self.factory.makePerson()
887+ self.task = self.factory.makeBugTask()
888+
889+ def test_user_transition_all_statuses(self):
890+ # A regular user should not be able to set statuses in
891+ # BUG_SUPERVISOR_BUGTASK_STATUSES, but can set any
892+ # other status.
893+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
894+ with person_logged_in(self.user):
895+ self.assertRaises(
896+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
897+ BugTaskStatus.WONTFIX, self.user)
898+ self.assertRaises(
899+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
900+ BugTaskStatus.EXPIRED, self.user)
901+ self.assertRaises(
902+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
903+ BugTaskStatus.TRIAGED, self.user)
904+ self.task.transitionToStatus(BugTaskStatus.NEW, self.user)
905+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
906+ self.task.transitionToStatus(
907+ BugTaskStatus.INCOMPLETE, self.user)
908+ self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
909+ self.task.transitionToStatus(BugTaskStatus.OPINION, self.user)
910+ self.assertEqual(self.task.status, BugTaskStatus.OPINION)
911+ self.task.transitionToStatus(BugTaskStatus.INVALID, self.user)
912+ self.assertEqual(self.task.status, BugTaskStatus.INVALID)
913+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user)
914+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
915+ self.task.transitionToStatus(
916+ BugTaskStatus.INPROGRESS, self.user)
917+ self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
918+ self.task.transitionToStatus(
919+ BugTaskStatus.FIXCOMMITTED, self.user)
920+ self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
921+ self.task.transitionToStatus(
922+ BugTaskStatus.FIXRELEASED, self.user)
923+ self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
924+
925+ def test_user_cannot_unset_wont_fix_status(self):
926+ # A regular user should not be able to transition a bug away
927+ # from Won't Fix.
928+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
929+ with person_logged_in(self.user):
930+ self.assertRaises(
931+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
932+ BugTaskStatus.CONFIRMED, self.user)
933+
934+ def test_user_cannot_unset_fix_released_status(self):
935+ # A regular user should not be able to transition a bug away
936+ # from Fix Released.
937+ removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
938+ with person_logged_in(self.user):
939+ self.assertRaises(
940+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
941+ BugTaskStatus.FIXRELEASED, self.user)
942+
943+ def test_user_canTransitionToStatus(self):
944+ # Regular user cannot transition to BUG_SUPERVISOR_BUGTASK_STATUSES,
945+ # but can transition to any other status.
946+ self.assertEqual(
947+ self.task.canTransitionToStatus(
948+ BugTaskStatus.WONTFIX, self.user),
949+ False)
950+ self.assertEqual(
951+ self.task.canTransitionToStatus(
952+ BugTaskStatus.EXPIRED, self.user),
953+ False)
954+ self.assertEqual(
955+ self.task.canTransitionToStatus(
956+ BugTaskStatus.TRIAGED, self.user),
957+ False)
958+ self.assertEqual(
959+ self.task.canTransitionToStatus(
960+ BugTaskStatus.NEW, self.user),
961+ True)
962+ self.assertEqual(
963+ self.task.canTransitionToStatus(
964+ BugTaskStatus.INCOMPLETE, self.user), True)
965+ self.assertEqual(
966+ self.task.canTransitionToStatus(
967+ BugTaskStatus.OPINION, self.user),
968+ True)
969+ self.assertEqual(
970+ self.task.canTransitionToStatus(
971+ BugTaskStatus.INVALID, self.user),
972+ True)
973+ self.assertEqual(
974+ self.task.canTransitionToStatus(
975+ BugTaskStatus.CONFIRMED, self.user),
976+ True)
977+ self.assertEqual(
978+ self.task.canTransitionToStatus(
979+ BugTaskStatus.INPROGRESS, self.user),
980+ True)
981+ self.assertEqual(
982+ self.task.canTransitionToStatus(
983+ BugTaskStatus.FIXCOMMITTED, self.user),
984+ True)
985+ self.assertEqual(
986+ self.task.canTransitionToStatus(
987+ BugTaskStatus.FIXRELEASED, self.user),
988+ True)
989+
990+ def test_user_canTransitionToStatus_from_wontfix(self):
991+ # A regular user cannot transition away from Won't Fix,
992+ # so canTransitionToStatus should return False.
993+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
994+ self.assertEqual(
995+ self.task.canTransitionToStatus(
996+ BugTaskStatus.NEW, self.user),
997+ False)
998+
999+ def test_user_canTransitionToStatus_from_fixreleased(self):
1000+ # A regular user cannot transition away from Fix Released,
1001+ # so canTransitionToStatus should return False.
1002+ removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1003+ self.assertEqual(
1004+ self.task.canTransitionToStatus(
1005+ BugTaskStatus.NEW, self.user),
1006+ False)
1007+
1008+
1009+class TestBugTaskStatusTransitionForPrivilegedUserBase:
1010+ """Base class used to test privileged users and status transitions."""
1011+
1012+ layer = LaunchpadFunctionalLayer
1013+
1014+ def setUp(self):
1015+ super(TestBugTaskStatusTransitionForPrivilegedUserBase, self).setUp()
1016+ # Creation of task and target are deferred to subclasses.
1017+ self.task = None
1018+ self.person = None
1019+ self.makePersonAndTask()
1020+
1021+ def makePersonAndTask(self):
1022+ """Create a bug task and privileged person for this task.
1023+
1024+ This method is implemented by subclasses to correctly setup
1025+ each test.
1026+ """
1027+ raise NotImplementedError(self.makePersonAndTask)
1028+
1029+ def test_privileged_user_transition_any_status(self):
1030+ # Privileged users (like owner or bug supervisor) should
1031+ # be able to set any status.
1032+ with person_logged_in(self.person):
1033+ self.task.transitionToStatus(BugTaskStatus.WONTFIX, self.person)
1034+ self.assertEqual(self.task.status, BugTaskStatus.WONTFIX)
1035+ self.task.transitionToStatus(BugTaskStatus.EXPIRED, self.person)
1036+ self.assertEqual(self.task.status, BugTaskStatus.EXPIRED)
1037+ self.task.transitionToStatus(BugTaskStatus.TRIAGED, self.person)
1038+ self.assertEqual(self.task.status, BugTaskStatus.TRIAGED)
1039+ self.task.transitionToStatus(BugTaskStatus.NEW, self.person)
1040+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
1041+ self.task.transitionToStatus(
1042+ BugTaskStatus.INCOMPLETE, self.person)
1043+ self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
1044+ self.task.transitionToStatus(BugTaskStatus.OPINION, self.person)
1045+ self.assertEqual(self.task.status, BugTaskStatus.OPINION)
1046+ self.task.transitionToStatus(BugTaskStatus.INVALID, self.person)
1047+ self.assertEqual(self.task.status, BugTaskStatus.INVALID)
1048+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1049+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1050+ self.task.transitionToStatus(
1051+ BugTaskStatus.INPROGRESS, self.person)
1052+ self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
1053+ self.task.transitionToStatus(
1054+ BugTaskStatus.FIXCOMMITTED, self.person)
1055+ self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
1056+ self.task.transitionToStatus(
1057+ BugTaskStatus.FIXRELEASED, self.person)
1058+ self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
1059+
1060+ def test_privileged_user_can_unset_wont_fix_status(self):
1061+ # Privileged users can transition away from Won't Fix.
1062+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1063+ with person_logged_in(self.person):
1064+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1065+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1066+
1067+ def test_privileged_user_can_unset_wont_fix_released(self):
1068+ # Privileged users can transition away from Fix Released.
1069+ removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1070+ with person_logged_in(self.person):
1071+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1072+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1073+
1074+ def test_privileged_user_canTransitionToStatus(self):
1075+ # Privileged users (like owner or bug supervisor) should
1076+ # be able to set any status, so canTransitionToStatus should
1077+ # always return True.
1078+ self.assertEqual(
1079+ self.task.canTransitionToStatus(
1080+ BugTaskStatus.WONTFIX, self.person),
1081+ True)
1082+ self.assertEqual(
1083+ self.task.canTransitionToStatus(
1084+ BugTaskStatus.EXPIRED, self.person),
1085+ True)
1086+ self.assertEqual(
1087+ self.task.canTransitionToStatus(
1088+ BugTaskStatus.TRIAGED, self.person),
1089+ True)
1090+ self.assertEqual(
1091+ self.task.canTransitionToStatus(
1092+ BugTaskStatus.NEW, self.person),
1093+ True)
1094+ self.assertEqual(
1095+ self.task.canTransitionToStatus(
1096+ BugTaskStatus.INCOMPLETE, self.person),
1097+ True)
1098+ self.assertEqual(
1099+ self.task.canTransitionToStatus(
1100+ BugTaskStatus.OPINION, self.person),
1101+ True)
1102+ self.assertEqual(
1103+ self.task.canTransitionToStatus(
1104+ BugTaskStatus.INVALID, self.person),
1105+ True)
1106+ self.assertEqual(
1107+ self.task.canTransitionToStatus(
1108+ BugTaskStatus.CONFIRMED, self.person),
1109+ True)
1110+ self.assertEqual(
1111+ self.task.canTransitionToStatus(
1112+ BugTaskStatus.INPROGRESS, self.person),
1113+ True)
1114+ self.assertEqual(
1115+ self.task.canTransitionToStatus(
1116+ BugTaskStatus.FIXCOMMITTED, self.person),
1117+ True)
1118+ self.assertEqual(
1119+ self.task.canTransitionToStatus(
1120+ BugTaskStatus.FIXRELEASED, self.person),
1121+ True)
1122+
1123+ def test_privileged_user_canTransitionToStatus_from_wontfix(self):
1124+ # A privileged user can transition away from Won't Fix, so
1125+ # canTransitionToStatus should return True.
1126+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1127+ self.assertEqual(
1128+ self.task.canTransitionToStatus(
1129+ BugTaskStatus.NEW, self.person),
1130+ True)
1131+
1132+ def test_privileged_user_canTransitionToStatus_from_fixreleased(self):
1133+ # A privileged user can transition away from Fix Released, so
1134+ # canTransitionToStatus should return True.
1135+ removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1136+ self.assertEqual(
1137+ self.task.canTransitionToStatus(
1138+ BugTaskStatus.NEW, self.person),
1139+ True)
1140+
1141+
1142+class TestBugTaskStatusTransitionOwnerPerson(
1143+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1144+ """Tests to ensure owner person can transition to any status.."""
1145+
1146+ def makePersonAndTask(self):
1147+ self.person = self.factory.makePerson()
1148+ self.product = self.factory.makeProduct(owner=self.person)
1149+ self.task = self.factory.makeBugTask(target=self.product)
1150+
1151+
1152+class TestBugTaskStatusTransitionOwnerTeam(
1153+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1154+ """Tests to ensure owner team can transition to any status.."""
1155+
1156+ def makePersonAndTask(self):
1157+ self.person = self.factory.makePerson()
1158+ self.team = self.factory.makeTeam(members=[self.person])
1159+ self.product = self.factory.makeProduct(owner=self.team)
1160+ self.task = self.factory.makeBugTask(target=self.product)
1161+
1162+
1163+class TestBugTaskStatusTransitionBugSupervisorPerson(
1164+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1165+ """Tests to ensure bug supervisor person can transition to any status."""
1166+
1167+ def makePersonAndTask(self):
1168+ self.owner = self.factory.makePerson()
1169+ self.person = self.factory.makePerson()
1170+ self.product = self.factory.makeProduct(owner=self.owner)
1171+ self.task = self.factory.makeBugTask(target=self.product)
1172+ with person_logged_in(self.owner):
1173+ self.product.setBugSupervisor(self.person, self.person)
1174+
1175+
1176+class TestBugTaskStatusTransitionBugSupervisorTeamMember(
1177+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1178+ """Tests to ensure bug supervisor team can transition to any status."""
1179+
1180+ def makePersonAndTask(self):
1181+ self.owner = self.factory.makePerson()
1182+ self.person = self.factory.makePerson()
1183+ self.team = self.factory.makeTeam(members=[self.person])
1184+ self.product = self.factory.makeProduct(owner=self.owner)
1185+ self.task = self.factory.makeBugTask(target=self.product)
1186+ with person_logged_in(self.owner):
1187+ self.product.setBugSupervisor(self.team, self.team)
1188+
1189+
1190+class TestBugTaskStatusTransitionBugWatchUpdater(
1191+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1192+ """Tests to ensure bug_watch_updater can transition to any status."""
1193+
1194+ def makePersonAndTask(self):
1195+ self.person = getUtility(ILaunchpadCelebrities).bug_watch_updater
1196+ self.task = self.factory.makeBugTask()
1197+
1198+
1199+class TestBugTaskStatusTransitionBugImporter(
1200+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1201+ """Tests to ensure bug_importer can transition to any status."""
1202+
1203+ def makePersonAndTask(self):
1204+ self.person = getUtility(ILaunchpadCelebrities).bug_importer
1205+ self.task = self.factory.makeBugTask()
1206+
1207+
1208+class TestBugTaskStatusTransitionJanitor(
1209+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1210+ """Tests to ensure lp janitor can transition to any status."""
1211+
1212+ def makePersonAndTask(self):
1213+ self.person = getUtility(ILaunchpadCelebrities).janitor
1214+ self.task = self.factory.makeBugTask()
1215
1216=== modified file 'lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt'
1217--- lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt 2010-10-18 22:24:59 +0000
1218+++ lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt 2010-10-25 11:13:20 +0000
1219@@ -17,7 +17,7 @@
1220
1221 Now if we go to the advanced search and choose to list only the bugs
1222 needing a bug watch, only the bugs with tasks in other contexts that
1223-don't use Launchpad Bugs are shown, if at least one of those contexts
1224+don't use Launchpad Bugs are shown, if at least one of those contexts
1225 doesn't have a bug watch.
1226
1227 # XXX: Bjorn Tillenius 2006-07-04 bug=51853:
1228@@ -97,7 +97,7 @@
1229 demonstrate.
1230
1231 >>> from canonical.launchpad.ftests import login, logout
1232- >>> from lp.bugs.tests.test_bugtask_1 import (
1233+ >>> from lp.bugs.model.tests.test_bugtask import (
1234 ... BugTaskSearchBugsElsewhereTest)
1235 >>> test_helper = BugTaskSearchBugsElsewhereTest(helper_only=True)
1236 >>> login('test@canonical.com')
1237@@ -181,8 +181,8 @@
1238 linux-source-2.6.15 Medium New
1239 2 Blackhole Trash folder
1240 — Medium New
1241-
1242-The user opens a bookmark for "upstream status: Show only bugs that need
1243+
1244+The user opens a bookmark for "upstream status: Show only bugs that need
1245 to be forwarded to an upstream bug tracker".
1246
1247 >>> bookmark_params['field.status_upstream'] = 'pending_bugwatch'
1248@@ -225,7 +225,7 @@
1249 ... bookmark_params, True))
1250 Traceback (most recent call last):
1251 ...
1252- UnexpectedFormData: Unexpected value for field 'status_upstream'.
1253+ UnexpectedFormData: Unexpected value for field 'status_upstream'.
1254 Perhaps your bookmarks are out of date or you changed the URL by hand?
1255
1256
1257
1258=== modified file 'lib/lp/bugs/subscribers/bug.py'
1259--- lib/lp/bugs/subscribers/bug.py 2010-08-23 09:25:17 +0000
1260+++ lib/lp/bugs/subscribers/bug.py 2010-10-25 11:13:20 +0000
1261@@ -19,6 +19,8 @@
1262 import datetime
1263 from operator import attrgetter
1264
1265+from zope.component import getUtility
1266+
1267 from canonical.config import config
1268 from canonical.database.sqlbase import block_implicit_flushes
1269 from canonical.launchpad.helpers import get_contact_email_addresses
1270@@ -34,14 +36,12 @@
1271 )
1272 from lp.bugs.adapters.bugdelta import BugDelta
1273 from lp.bugs.interfaces.bugchange import IBugChange
1274+from lp.bugs.interfaces.bugtask import IBugTaskSet
1275 from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
1276 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
1277 from lp.bugs.mail.newbug import generate_bug_add_email
1278 from lp.registry.enum import BugNotificationLevel
1279 from lp.registry.interfaces.person import IPerson
1280-from lp.registry.interfaces.structuralsubscription import (
1281- IStructuralSubscriptionTarget,
1282- )
1283
1284
1285 @block_implicit_flushes
1286@@ -179,15 +179,10 @@
1287 if recipients is not None:
1288 recipients.addAssignee(bugtask.assignee)
1289
1290- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
1291- also_notified_subscribers.update(
1292- bugtask.target.getBugNotificationsRecipients(
1293- recipients, level=level))
1294-
1295- if bugtask.milestone is not None:
1296- also_notified_subscribers.update(
1297- bugtask.milestone.getBugNotificationsRecipients(
1298- recipients, level=level))
1299+ # Get structural subscribers.
1300+ also_notified_subscribers.update(
1301+ getUtility(IBugTaskSet).getStructuralSubscribers(
1302+ [bugtask], recipients, level))
1303
1304 # If the target's bug supervisor isn't set,
1305 # we add the owner as a subscriber.
1306
1307=== removed file 'lib/lp/bugs/tests/test_bugtask_0.py'
1308--- lib/lp/bugs/tests/test_bugtask_0.py 2010-10-21 04:19:36 +0000
1309+++ lib/lp/bugs/tests/test_bugtask_0.py 1970-01-01 00:00:00 +0000
1310@@ -1,36 +0,0 @@
1311-# Copyright 2009 Canonical Ltd. This software is licensed under the
1312-# GNU Affero General Public License version 3 (see the file LICENSE).
1313-
1314-"""Tests for bugtask.py."""
1315-
1316-__metaclass__ = type
1317-
1318-from doctest import (
1319- DocTestSuite,
1320- ELLIPSIS,
1321- NORMALIZE_WHITESPACE,
1322- REPORT_NDIFF,
1323- )
1324-
1325-
1326-def test_open_and_resolved_statuses(self):
1327- """
1328- There are constants that are used to define which statuses are for
1329- resolved bugs (RESOLVED_BUGTASK_STATUSES), and which are for
1330- unresolved bugs (UNRESOLVED_BUGTASK_STATUSES). The two constants
1331- include all statuses defined in BugTaskStatus, except for Unknown.
1332-
1333- >>> from lp.bugs.interfaces.bugtask import (
1334- ... BugTaskStatus, RESOLVED_BUGTASK_STATUSES,
1335- ... UNRESOLVED_BUGTASK_STATUSES)
1336- >>> not_included_status = set(BugTaskStatus.items).difference(
1337- ... RESOLVED_BUGTASK_STATUSES + UNRESOLVED_BUGTASK_STATUSES)
1338- >>> [status.name for status in not_included_status]
1339- ['UNKNOWN']
1340- """
1341-
1342-
1343-def test_suite():
1344- suite = DocTestSuite(
1345- optionflags=REPORT_NDIFF|NORMALIZE_WHITESPACE|ELLIPSIS)
1346- return suite
1347
1348=== removed file 'lib/lp/bugs/tests/test_bugtask_1.py'
1349--- lib/lp/bugs/tests/test_bugtask_1.py 2010-10-21 12:43:32 +0000
1350+++ lib/lp/bugs/tests/test_bugtask_1.py 1970-01-01 00:00:00 +0000
1351@@ -1,345 +0,0 @@
1352-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
1353-# GNU Affero General Public License version 3 (see the file LICENSE).
1354-
1355-"""Bugtask related tests that are too complex to be readable as doctests."""
1356-
1357-__metaclass__ = type
1358-
1359-import unittest
1360-
1361-from zope.component import getUtility
1362-
1363-from canonical.database.sqlbase import flush_database_updates
1364-from canonical.launchpad.ftests import (
1365- ANONYMOUS,
1366- login,
1367- logout,
1368- )
1369-from canonical.launchpad.webapp.interfaces import ILaunchBag
1370-from canonical.testing.layers import DatabaseFunctionalLayer
1371-from lp.app.enums import ServiceUsage
1372-from lp.bugs.interfaces.bug import IBugSet
1373-from lp.bugs.interfaces.bugtask import (
1374- BugTaskStatus,
1375- IBugTaskSet,
1376- IUpstreamBugTask,
1377- )
1378-from lp.bugs.interfaces.bugwatch import IBugWatchSet
1379-from lp.bugs.tests.bug import (
1380- create_old_bug,
1381- sync_bugtasks,
1382- )
1383-from lp.registry.interfaces.distribution import IDistributionSet
1384-from lp.registry.interfaces.product import IProductSet
1385-from lp.registry.interfaces.projectgroup import IProjectGroupSet
1386-from lp.testing.factory import LaunchpadObjectFactory
1387-
1388-
1389-class BugTaskSearchBugsElsewhereTest(unittest.TestCase):
1390- """Tests for searching bugs filtering on related bug tasks.
1391-
1392- It also acts as a helper class, which makes related doctests more
1393- readable, since they can use methods from this class.
1394- """
1395- layer = DatabaseFunctionalLayer
1396-
1397- def __init__(self, methodName='runTest', helper_only=False):
1398- """If helper_only is True, set up it only as a helper class."""
1399- if not helper_only:
1400- unittest.TestCase.__init__(self, methodName=methodName)
1401-
1402- def setUp(self):
1403- login(ANONYMOUS)
1404-
1405- def tearDown(self):
1406- logout()
1407-
1408- def _getBugTaskByTarget(self, bug, target):
1409- """Return a bug's bugtask for the given target."""
1410- for bugtask in bug.bugtasks:
1411- if bugtask.target == target:
1412- return bugtask
1413- else:
1414- raise AssertionError(
1415- "Didn't find a %s task on bug %s." % (
1416- target.bugtargetname, bug.id))
1417-
1418- def setUpBugsResolvedUpstreamTests(self):
1419- """Modify some bugtasks to match the resolved upstream filter."""
1420- bugset = getUtility(IBugSet)
1421- productset = getUtility(IProductSet)
1422- firefox = productset.getByName("firefox")
1423- thunderbird = productset.getByName("thunderbird")
1424-
1425- # Mark an upstream task on bug #1 "Fix Released"
1426- bug_one = bugset.get(1)
1427- firefox_upstream = self._getBugTaskByTarget(bug_one, firefox)
1428- self.assertEqual(
1429- ServiceUsage.LAUNCHPAD,
1430- firefox_upstream.product.bug_tracking_usage)
1431- self.old_firefox_status = firefox_upstream.status
1432- firefox_upstream.transitionToStatus(
1433- BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
1434- self.firefox_upstream = firefox_upstream
1435-
1436- # Mark an upstream task on bug #9 "Fix Committed"
1437- bug_nine = bugset.get(9)
1438- thunderbird_upstream = self._getBugTaskByTarget(bug_nine, thunderbird)
1439- self.old_thunderbird_status = thunderbird_upstream.status
1440- thunderbird_upstream.transitionToStatus(
1441- BugTaskStatus.FIXCOMMITTED, getUtility(ILaunchBag).user)
1442- self.thunderbird_upstream = thunderbird_upstream
1443-
1444- # Add a watch to a Debian bug for bug #2, and mark the task Fix
1445- # Released.
1446- bug_two = bugset.get(2)
1447- current_user = getUtility(ILaunchBag).user
1448- bugtaskset = getUtility(IBugTaskSet)
1449- bugwatchset = getUtility(IBugWatchSet)
1450-
1451- # Get a debbugs watch.
1452- watch_debbugs_327452 = bugwatchset.get(9)
1453- self.assertEquals(watch_debbugs_327452.bugtracker.name, "debbugs")
1454- self.assertEquals(watch_debbugs_327452.remotebug, "327452")
1455-
1456- # Associate the watch to a Fix Released task.
1457- debian = getUtility(IDistributionSet).getByName("debian")
1458- debian_firefox = debian.getSourcePackage("mozilla-firefox")
1459- bug_two_in_debian_firefox = self._getBugTaskByTarget(
1460- bug_two, debian_firefox)
1461- bug_two_in_debian_firefox.bugwatch = watch_debbugs_327452
1462- bug_two_in_debian_firefox.transitionToStatus(
1463- BugTaskStatus.FIXRELEASED, getUtility(ILaunchBag).user)
1464-
1465- flush_database_updates()
1466-
1467- def tearDownBugsElsewhereTests(self):
1468- """Resets the modified bugtasks to their original statuses."""
1469- self.firefox_upstream.transitionToStatus(
1470- self.old_firefox_status,
1471- self.firefox_upstream.target.bug_supervisor)
1472- self.thunderbird_upstream.transitionToStatus(
1473- self.old_thunderbird_status,
1474- self.firefox_upstream.target.bug_supervisor)
1475- flush_database_updates()
1476-
1477- def assertBugTaskIsPendingBugWatchElsewhere(self, bugtask):
1478- """Assert the bugtask is pending a bug watch elsewhere.
1479-
1480- Pending a bugwatch elsewhere means that at least one of the bugtask's
1481- related task's target isn't using Malone, and that
1482- related_bugtask.bugwatch is None.
1483- """
1484- non_malone_using_bugtasks = [
1485- related_task for related_task in bugtask.related_tasks
1486- if not related_task.target_uses_malone]
1487- pending_bugwatch_bugtasks = [
1488- related_bugtask for related_bugtask in non_malone_using_bugtasks
1489- if related_bugtask.bugwatch is None]
1490- self.assert_(
1491- len(pending_bugwatch_bugtasks) > 0,
1492- 'Bugtask %s on %s has no related bug watches elsewhere.' % (
1493- bugtask.id, bugtask.target.displayname))
1494-
1495- def assertBugTaskIsResolvedUpstream(self, bugtask):
1496- """Make sure at least one of the related upstream tasks is resolved.
1497-
1498- "Resolved", for our purposes, means either that one of the related
1499- tasks is an upstream task in FIXCOMMITTED or FIXRELEASED state, or
1500- it is a task with a bugwatch, and in FIXCOMMITTED, FIXRELEASED, or
1501- INVALID state.
1502- """
1503- resolved_upstream_states = [
1504- BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED]
1505- resolved_bugwatch_states = [
1506- BugTaskStatus.FIXCOMMITTED, BugTaskStatus.FIXRELEASED,
1507- BugTaskStatus.INVALID]
1508-
1509- # Helper functions for the list comprehension below.
1510- def _is_resolved_upstream_task(bugtask):
1511- return (
1512- IUpstreamBugTask.providedBy(bugtask) and
1513- bugtask.status in resolved_upstream_states)
1514-
1515- def _is_resolved_bugwatch_task(bugtask):
1516- return (
1517- bugtask.bugwatch and bugtask.status in
1518- resolved_bugwatch_states)
1519-
1520- resolved_related_tasks = [
1521- related_task for related_task in bugtask.related_tasks
1522- if (_is_resolved_upstream_task(related_task) or
1523- _is_resolved_bugwatch_task(related_task))]
1524-
1525- self.assert_(len(resolved_related_tasks) > 0)
1526- self.assert_(
1527- len(resolved_related_tasks) > 0,
1528- 'Bugtask %s on %s has no resolved related tasks.' % (
1529- bugtask.id, bugtask.target.displayname))
1530-
1531- def assertBugTaskIsOpenUpstream(self, bugtask):
1532- """Make sure at least one of the related upstream tasks is open.
1533-
1534- "Open", for our purposes, means either that one of the related
1535- tasks is an upstream task or a task with a bugwatch which has
1536- one of the states listed in open_states.
1537- """
1538- open_states = [
1539- BugTaskStatus.NEW,
1540- BugTaskStatus.INCOMPLETE,
1541- BugTaskStatus.CONFIRMED,
1542- BugTaskStatus.INPROGRESS,
1543- BugTaskStatus.UNKNOWN]
1544-
1545- # Helper functions for the list comprehension below.
1546- def _is_open_upstream_task(bugtask):
1547- return (
1548- IUpstreamBugTask.providedBy(bugtask) and
1549- bugtask.status in open_states)
1550-
1551- def _is_open_bugwatch_task(bugtask):
1552- return (
1553- bugtask.bugwatch and bugtask.status in
1554- open_states)
1555-
1556- open_related_tasks = [
1557- related_task for related_task in bugtask.related_tasks
1558- if (_is_open_upstream_task(related_task) or
1559- _is_open_bugwatch_task(related_task))]
1560-
1561- self.assert_(
1562- len(open_related_tasks) > 0,
1563- 'Bugtask %s on %s has no open related tasks.' % (
1564- bugtask.id, bugtask.target.displayname))
1565-
1566- def _hasUpstreamTask(self, bug):
1567- """Does this bug have an upstream task associated with it?
1568-
1569- Returns True if yes, otherwise False.
1570- """
1571- for bugtask in bug.bugtasks:
1572- if IUpstreamBugTask.providedBy(bugtask):
1573- return True
1574- return False
1575-
1576- def assertShouldBeShownOnNoUpstreamTaskSearch(self, bugtask):
1577- """Should the bugtask be shown in the search no upstream task search?
1578-
1579- Returns True if yes, otherwise False.
1580- """
1581- self.assert_(
1582- not self._hasUpstreamTask(bugtask.bug),
1583- 'Bugtask %s on %s has upstream tasks.' % (
1584- bugtask.id, bugtask.target.displayname))
1585-
1586-
1587-class BugTaskSetFindExpirableBugTasksTest(unittest.TestCase):
1588- """Test `BugTaskSet.findExpirableBugTasks()` behaviour."""
1589- layer = DatabaseFunctionalLayer
1590-
1591- def setUp(self):
1592- """Setup the zope interaction and create expirable bugtasks."""
1593- login('test@canonical.com')
1594- self.user = getUtility(ILaunchBag).user
1595- self.distribution = getUtility(IDistributionSet).getByName('ubuntu')
1596- self.distroseries = self.distribution.getSeries('hoary')
1597- self.product = getUtility(IProductSet).getByName('jokosher')
1598- self.productseries = self.product.getSeries('trunk')
1599- self.bugtaskset = getUtility(IBugTaskSet)
1600- bugtasks = []
1601- bugtasks.append(
1602- create_old_bug("90 days old", 90, self.distribution))
1603- bugtasks.append(
1604- self.bugtaskset.createTask(
1605- bug=bugtasks[-1].bug, owner=self.user,
1606- distroseries=self.distroseries))
1607- bugtasks.append(
1608- create_old_bug("90 days old", 90, self.product))
1609- bugtasks.append(
1610- self.bugtaskset.createTask(
1611- bug=bugtasks[-1].bug, owner=self.user,
1612- productseries=self.productseries))
1613- sync_bugtasks(bugtasks)
1614-
1615- def tearDown(self):
1616- logout()
1617-
1618- def testSupportedTargetParam(self):
1619- """The target param supports a limited set of BugTargets.
1620-
1621- Four BugTarget types may passed as the target argument:
1622- Distribution, DistroSeries, Product, ProductSeries.
1623- """
1624- supported_targets = [self.distribution, self.distroseries,
1625- self.product, self.productseries]
1626- for target in supported_targets:
1627- expirable_bugtasks = self.bugtaskset.findExpirableBugTasks(
1628- 0, self.user, target=target)
1629- self.assertNotEqual(expirable_bugtasks.count(), 0,
1630- "%s has %d expirable bugtasks." %
1631- (self.distroseries, expirable_bugtasks.count()))
1632-
1633- def testUnsupportedBugTargetParam(self):
1634- """Test that unsupported targets raise errors.
1635-
1636- Three BugTarget types are not supported because the UI does not
1637- provide bug-index to link to the 'bugs that can expire' page.
1638- ProjectGroup, SourcePackage, and DistributionSourcePackage will
1639- raise an NotImplementedError.
1640-
1641- Passing an unknown bugtarget type will raise an AssertionError.
1642- """
1643- project = getUtility(IProjectGroupSet).getByName('mozilla')
1644- distributionsourcepackage = self.distribution.getSourcePackage(
1645- 'mozilla-firefox')
1646- sourcepackage = self.distroseries.getSourcePackage(
1647- 'mozilla-firefox')
1648- unsupported_targets = [project, distributionsourcepackage,
1649- sourcepackage]
1650- for target in unsupported_targets:
1651- self.assertRaises(
1652- NotImplementedError, self.bugtaskset.findExpirableBugTasks,
1653- 0, self.user, target=target)
1654-
1655- # Objects that are not a known BugTarget type raise an AssertionError.
1656- self.assertRaises(
1657- AssertionError, self.bugtaskset.findExpirableBugTasks,
1658- 0, self.user, target=[])
1659-
1660-
1661-class BugTaskSetTest(unittest.TestCase):
1662- """Test `BugTaskSet` methods."""
1663- layer = DatabaseFunctionalLayer
1664-
1665- def setUp(self):
1666- login(ANONYMOUS)
1667-
1668- def test_getBugTasks(self):
1669- """ IBugTaskSet.getBugTasks() returns a dictionary mapping the given
1670- bugs to their bugtasks. It does that in a single query, to avoid
1671- hitting the DB again when getting the bugs' tasks.
1672- """
1673- login('no-priv@canonical.com')
1674- factory = LaunchpadObjectFactory()
1675- bug1 = factory.makeBug()
1676- factory.makeBugTask(bug1)
1677- bug2 = factory.makeBug()
1678- factory.makeBugTask(bug2)
1679- factory.makeBugTask(bug2)
1680-
1681- bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks(
1682- [bug1.id, bug2.id])
1683- # The bugtasks returned by getBugTasks() are exactly the same as the
1684- # ones returned by bug.bugtasks, obviously.
1685- self.failUnlessEqual(
1686- set(bugs_and_tasks[bug1]).difference(bug1.bugtasks),
1687- set([]))
1688- self.failUnlessEqual(
1689- set(bugs_and_tasks[bug2]).difference(bug2.bugtasks),
1690- set([]))
1691-
1692- def test_getBugTasks_with_empty_list(self):
1693- # When given an empty list of bug IDs, getBugTasks() will return an
1694- # empty dictionary.
1695- bugs_and_tasks = getUtility(IBugTaskSet).getBugTasks([])
1696- self.failUnlessEqual(bugs_and_tasks, {})
1697
1698=== removed file 'lib/lp/bugs/tests/test_bugtask_status.py'
1699--- lib/lp/bugs/tests/test_bugtask_status.py 2010-10-19 19:31:07 +0000
1700+++ lib/lp/bugs/tests/test_bugtask_status.py 1970-01-01 00:00:00 +0000
1701@@ -1,356 +0,0 @@
1702-# Copyright 2009 Canonical Ltd. This software is licensed under the
1703-# GNU Affero General Public License version 3 (see the file LICENSE).
1704-
1705-"""Tests for bug task status transitions."""
1706-
1707-__metaclass__ = type
1708-
1709-from zope.component import getUtility
1710-from zope.security.proxy import removeSecurityProxy
1711-
1712-from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1713-from canonical.testing.layers import LaunchpadFunctionalLayer
1714-from lp.bugs.interfaces.bugtask import UserCannotEditBugTaskStatus
1715-from lp.bugs.model.bugtask import BugTaskStatus
1716-from lp.testing import (
1717- person_logged_in,
1718- TestCaseWithFactory,
1719- )
1720-
1721-
1722-class TestBugTaskStatusTransitionForUser(TestCaseWithFactory):
1723- """Test bugtask status transitions for a regular logged in user."""
1724-
1725- layer = LaunchpadFunctionalLayer
1726-
1727- def setUp(self):
1728- super(TestBugTaskStatusTransitionForUser, self).setUp()
1729- self.user = self.factory.makePerson()
1730- self.task = self.factory.makeBugTask()
1731-
1732- def test_user_transition_all_statuses(self):
1733- # A regular user should not be able to set statuses in
1734- # BUG_SUPERVISOR_BUGTASK_STATUSES, but can set any
1735- # other status.
1736- self.assertEqual(self.task.status, BugTaskStatus.NEW)
1737- with person_logged_in(self.user):
1738- self.assertRaises(
1739- UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1740- BugTaskStatus.WONTFIX, self.user)
1741- self.assertRaises(
1742- UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1743- BugTaskStatus.EXPIRED, self.user)
1744- self.assertRaises(
1745- UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1746- BugTaskStatus.TRIAGED, self.user)
1747- self.task.transitionToStatus(BugTaskStatus.NEW, self.user)
1748- self.assertEqual(self.task.status, BugTaskStatus.NEW)
1749- self.task.transitionToStatus(
1750- BugTaskStatus.INCOMPLETE, self.user)
1751- self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
1752- self.task.transitionToStatus(BugTaskStatus.OPINION, self.user)
1753- self.assertEqual(self.task.status, BugTaskStatus.OPINION)
1754- self.task.transitionToStatus(BugTaskStatus.INVALID, self.user)
1755- self.assertEqual(self.task.status, BugTaskStatus.INVALID)
1756- self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user)
1757- self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1758- self.task.transitionToStatus(
1759- BugTaskStatus.INPROGRESS, self.user)
1760- self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
1761- self.task.transitionToStatus(
1762- BugTaskStatus.FIXCOMMITTED, self.user)
1763- self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
1764- self.task.transitionToStatus(
1765- BugTaskStatus.FIXRELEASED, self.user)
1766- self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
1767-
1768- def test_user_cannot_unset_wont_fix_status(self):
1769- # A regular user should not be able to transition a bug away
1770- # from Won't Fix.
1771- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1772- with person_logged_in(self.user):
1773- self.assertRaises(
1774- UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1775- BugTaskStatus.CONFIRMED, self.user)
1776-
1777- def test_user_cannot_unset_fix_released_status(self):
1778- # A regular user should not be able to transition a bug away
1779- # from Fix Released.
1780- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1781- with person_logged_in(self.user):
1782- self.assertRaises(
1783- UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1784- BugTaskStatus.FIXRELEASED, self.user)
1785-
1786- def test_user_canTransitionToStatus(self):
1787- # Regular user cannot transition to BUG_SUPERVISOR_BUGTASK_STATUSES,
1788- # but can transition to any other status.
1789- self.assertEqual(
1790- self.task.canTransitionToStatus(
1791- BugTaskStatus.WONTFIX, self.user),
1792- False)
1793- self.assertEqual(
1794- self.task.canTransitionToStatus(
1795- BugTaskStatus.EXPIRED, self.user),
1796- False)
1797- self.assertEqual(
1798- self.task.canTransitionToStatus(
1799- BugTaskStatus.TRIAGED, self.user),
1800- False)
1801- self.assertEqual(
1802- self.task.canTransitionToStatus(
1803- BugTaskStatus.NEW, self.user),
1804- True)
1805- self.assertEqual(
1806- self.task.canTransitionToStatus(
1807- BugTaskStatus.INCOMPLETE, self.user), True)
1808- self.assertEqual(
1809- self.task.canTransitionToStatus(
1810- BugTaskStatus.OPINION, self.user),
1811- True)
1812- self.assertEqual(
1813- self.task.canTransitionToStatus(
1814- BugTaskStatus.INVALID, self.user),
1815- True)
1816- self.assertEqual(
1817- self.task.canTransitionToStatus(
1818- BugTaskStatus.CONFIRMED, self.user),
1819- True)
1820- self.assertEqual(
1821- self.task.canTransitionToStatus(
1822- BugTaskStatus.INPROGRESS, self.user),
1823- True)
1824- self.assertEqual(
1825- self.task.canTransitionToStatus(
1826- BugTaskStatus.FIXCOMMITTED, self.user),
1827- True)
1828- self.assertEqual(
1829- self.task.canTransitionToStatus(
1830- BugTaskStatus.FIXRELEASED, self.user),
1831- True)
1832-
1833- def test_user_canTransitionToStatus_from_wontfix(self):
1834- # A regular user cannot transition away from Won't Fix,
1835- # so canTransitionToStatus should return False.
1836- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1837- self.assertEqual(
1838- self.task.canTransitionToStatus(
1839- BugTaskStatus.NEW, self.user),
1840- False)
1841-
1842- def test_user_canTransitionToStatus_from_fixreleased(self):
1843- # A regular user cannot transition away from Fix Released,
1844- # so canTransitionToStatus should return False.
1845- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1846- self.assertEqual(
1847- self.task.canTransitionToStatus(
1848- BugTaskStatus.NEW, self.user),
1849- False)
1850-
1851-
1852-class TestBugTaskStatusTransitionForPrivilegedUserBase:
1853- """Base class used to test privileged users and status transitions."""
1854-
1855- layer = LaunchpadFunctionalLayer
1856-
1857- def setUp(self):
1858- super(TestBugTaskStatusTransitionForPrivilegedUserBase, self).setUp()
1859- # Creation of task and target are deferred to subclasses.
1860- self.task = None
1861- self.person = None
1862- self.makePersonAndTask()
1863-
1864- def makePersonAndTask(self):
1865- """Create a bug task and privileged person for this task.
1866-
1867- This method is implemented by subclasses to correctly setup
1868- each test.
1869- """
1870- raise NotImplementedError(self.makePersonAndTask)
1871-
1872- def test_privileged_user_transition_any_status(self):
1873- # Privileged users (like owner or bug supervisor) should
1874- # be able to set any status.
1875- with person_logged_in(self.person):
1876- self.task.transitionToStatus(BugTaskStatus.WONTFIX, self.person)
1877- self.assertEqual(self.task.status, BugTaskStatus.WONTFIX)
1878- self.task.transitionToStatus(BugTaskStatus.EXPIRED, self.person)
1879- self.assertEqual(self.task.status, BugTaskStatus.EXPIRED)
1880- self.task.transitionToStatus(BugTaskStatus.TRIAGED, self.person)
1881- self.assertEqual(self.task.status, BugTaskStatus.TRIAGED)
1882- self.task.transitionToStatus(BugTaskStatus.NEW, self.person)
1883- self.assertEqual(self.task.status, BugTaskStatus.NEW)
1884- self.task.transitionToStatus(
1885- BugTaskStatus.INCOMPLETE, self.person)
1886- self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
1887- self.task.transitionToStatus(BugTaskStatus.OPINION, self.person)
1888- self.assertEqual(self.task.status, BugTaskStatus.OPINION)
1889- self.task.transitionToStatus(BugTaskStatus.INVALID, self.person)
1890- self.assertEqual(self.task.status, BugTaskStatus.INVALID)
1891- self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1892- self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1893- self.task.transitionToStatus(
1894- BugTaskStatus.INPROGRESS, self.person)
1895- self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
1896- self.task.transitionToStatus(
1897- BugTaskStatus.FIXCOMMITTED, self.person)
1898- self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
1899- self.task.transitionToStatus(
1900- BugTaskStatus.FIXRELEASED, self.person)
1901- self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
1902-
1903- def test_privileged_user_can_unset_wont_fix_status(self):
1904- # Privileged users can transition away from Won't Fix.
1905- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1906- with person_logged_in(self.person):
1907- self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1908- self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1909-
1910- def test_privileged_user_can_unset_wont_fix_released(self):
1911- # Privileged users can transition away from Fix Released.
1912- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1913- with person_logged_in(self.person):
1914- self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1915- self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1916-
1917- def test_privileged_user_canTransitionToStatus(self):
1918- # Privileged users (like owner or bug supervisor) should
1919- # be able to set any status, so canTransitionToStatus should
1920- # always return True.
1921- self.assertEqual(
1922- self.task.canTransitionToStatus(
1923- BugTaskStatus.WONTFIX, self.person),
1924- True)
1925- self.assertEqual(
1926- self.task.canTransitionToStatus(
1927- BugTaskStatus.EXPIRED, self.person),
1928- True)
1929- self.assertEqual(
1930- self.task.canTransitionToStatus(
1931- BugTaskStatus.TRIAGED, self.person),
1932- True)
1933- self.assertEqual(
1934- self.task.canTransitionToStatus(
1935- BugTaskStatus.NEW, self.person),
1936- True)
1937- self.assertEqual(
1938- self.task.canTransitionToStatus(
1939- BugTaskStatus.INCOMPLETE, self.person),
1940- True)
1941- self.assertEqual(
1942- self.task.canTransitionToStatus(
1943- BugTaskStatus.OPINION, self.person),
1944- True)
1945- self.assertEqual(
1946- self.task.canTransitionToStatus(
1947- BugTaskStatus.INVALID, self.person),
1948- True)
1949- self.assertEqual(
1950- self.task.canTransitionToStatus(
1951- BugTaskStatus.CONFIRMED, self.person),
1952- True)
1953- self.assertEqual(
1954- self.task.canTransitionToStatus(
1955- BugTaskStatus.INPROGRESS, self.person),
1956- True)
1957- self.assertEqual(
1958- self.task.canTransitionToStatus(
1959- BugTaskStatus.FIXCOMMITTED, self.person),
1960- True)
1961- self.assertEqual(
1962- self.task.canTransitionToStatus(
1963- BugTaskStatus.FIXRELEASED, self.person),
1964- True)
1965-
1966- def test_privileged_user_canTransitionToStatus_from_wontfix(self):
1967- # A privileged user can transition away from Won't Fix, so
1968- # canTransitionToStatus should return True.
1969- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1970- self.assertEqual(
1971- self.task.canTransitionToStatus(
1972- BugTaskStatus.NEW, self.person),
1973- True)
1974-
1975- def test_privileged_user_canTransitionToStatus_from_fixreleased(self):
1976- # A privileged user can transition away from Fix Released, so
1977- # canTransitionToStatus should return True.
1978- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1979- self.assertEqual(
1980- self.task.canTransitionToStatus(
1981- BugTaskStatus.NEW, self.person),
1982- True)
1983-
1984-
1985-class TestBugTaskStatusTransitionOwnerPerson(
1986- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1987- """Tests to ensure owner person can transition to any status.."""
1988-
1989- def makePersonAndTask(self):
1990- self.person = self.factory.makePerson()
1991- self.product = self.factory.makeProduct(owner=self.person)
1992- self.task = self.factory.makeBugTask(target=self.product)
1993-
1994-
1995-class TestBugTaskStatusTransitionOwnerTeam(
1996- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
1997- """Tests to ensure owner team can transition to any status.."""
1998-
1999- def makePersonAndTask(self):
2000- self.person = self.factory.makePerson()
2001- self.team = self.factory.makeTeam(members=[self.person])
2002- self.product = self.factory.makeProduct(owner=self.team)
2003- self.task = self.factory.makeBugTask(target=self.product)
2004-
2005-
2006-class TestBugTaskStatusTransitionBugSupervisorPerson(
2007- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
2008- """Tests to ensure bug supervisor person can transition to any status."""
2009-
2010- def makePersonAndTask(self):
2011- self.owner = self.factory.makePerson()
2012- self.person = self.factory.makePerson()
2013- self.product = self.factory.makeProduct(owner=self.owner)
2014- self.task = self.factory.makeBugTask(target=self.product)
2015- with person_logged_in(self.owner):
2016- self.product.setBugSupervisor(self.person, self.person)
2017-
2018-
2019-class TestBugTaskStatusTransitionBugSupervisorTeamMember(
2020- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
2021- """Tests to ensure bug supervisor team can transition to any status."""
2022-
2023- def makePersonAndTask(self):
2024- self.owner = self.factory.makePerson()
2025- self.person = self.factory.makePerson()
2026- self.team = self.factory.makeTeam(members=[self.person])
2027- self.product = self.factory.makeProduct(owner=self.owner)
2028- self.task = self.factory.makeBugTask(target=self.product)
2029- with person_logged_in(self.owner):
2030- self.product.setBugSupervisor(self.team, self.team)
2031-
2032-
2033-class TestBugTaskStatusTransitionBugWatchUpdater(
2034- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
2035- """Tests to ensure bug_watch_updater can transition to any status."""
2036-
2037- def makePersonAndTask(self):
2038- self.person = getUtility(ILaunchpadCelebrities).bug_watch_updater
2039- self.task = self.factory.makeBugTask()
2040-
2041-
2042-class TestBugTaskStatusTransitionBugImporter(
2043- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
2044- """Tests to ensure bug_importer can transition to any status."""
2045-
2046- def makePersonAndTask(self):
2047- self.person = getUtility(ILaunchpadCelebrities).bug_importer
2048- self.task = self.factory.makeBugTask()
2049-
2050-
2051-class TestBugTaskStatusTransitionJanitor(
2052- TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
2053- """Tests to ensure lp janitor can transition to any status."""
2054-
2055- def makePersonAndTask(self):
2056- self.person = getUtility(ILaunchpadCelebrities).janitor
2057- self.task = self.factory.makeBugTask()
2058
2059=== added file 'lib/lp/code/interfaces/branchmergequeue.py'
2060--- lib/lp/code/interfaces/branchmergequeue.py 1970-01-01 00:00:00 +0000
2061+++ lib/lp/code/interfaces/branchmergequeue.py 2010-10-20 15:32:38 +0000
2062@@ -0,0 +1,115 @@
2063+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2064+# GNU Affero General Public License version 3 (see the file LICENSE).
2065+
2066+"""Branch merge queue interfaces."""
2067+
2068+__metaclass__ = type
2069+
2070+__all__ = [
2071+ 'IBranchMergeQueue',
2072+ 'IBranchMergeQueueSource',
2073+ ]
2074+
2075+from lazr.restful.declarations import (
2076+ export_as_webservice_entry,
2077+ export_write_operation,
2078+ exported,
2079+ mutator_for,
2080+ operation_parameters,
2081+ )
2082+from lazr.restful.fields import (
2083+ CollectionField,
2084+ Reference,
2085+ )
2086+from zope.interface import Interface
2087+from zope.schema import (
2088+ Datetime,
2089+ Int,
2090+ Text,
2091+ TextLine,
2092+ )
2093+
2094+from canonical.launchpad import _
2095+from lp.services.fields import (
2096+ PersonChoice,
2097+ PublicPersonChoice,
2098+ )
2099+
2100+
2101+class IBranchMergeQueue(Interface):
2102+ """An interface for managing branch merges."""
2103+
2104+ export_as_webservice_entry()
2105+
2106+ id = Int(title=_('ID'), readonly=True, required=True)
2107+
2108+ registrant = exported(
2109+ PublicPersonChoice(
2110+ title=_("The user that registered the branch."),
2111+ required=True, readonly=True,
2112+ vocabulary='ValidPersonOrTeam'))
2113+
2114+ owner = exported(
2115+ PersonChoice(
2116+ title=_('Owner'),
2117+ required=True, readonly=True,
2118+ vocabulary='UserTeamsParticipationPlusSelf',
2119+ description=_("The owner of the merge queue.")))
2120+
2121+ name = exported(
2122+ TextLine(
2123+ title=_('Name'), required=True,
2124+ description=_(
2125+ "Keep very short, unique, and descriptive, because it will "
2126+ "be used in URLs. "
2127+ "Examples: main, devel, release-1.0, gnome-vfs.")))
2128+
2129+ description = exported(
2130+ Text(
2131+ title=_('Description'), required=False,
2132+ description=_(
2133+ 'A short description of the purpose of this merge queue.')))
2134+
2135+ configuration = exported(
2136+ TextLine(
2137+ title=_('Configuration'), required=False, readonly=True,
2138+ description=_(
2139+ "A JSON string of configuration values.")))
2140+
2141+ date_created = exported(
2142+ Datetime(
2143+ title=_('Date Created'),
2144+ required=True,
2145+ readonly=True))
2146+
2147+ branches = exported(
2148+ CollectionField(
2149+ title=_('Dependent Branches'),
2150+ description=_(
2151+ 'A collection of branches that this queue manages.'),
2152+ readonly=True,
2153+ value_type=Reference(Interface)))
2154+
2155+ @mutator_for(configuration)
2156+ @operation_parameters(
2157+ config=TextLine(title=_("A JSON string of configuration values.")))
2158+ @export_write_operation()
2159+ def setMergeQueueConfig(config):
2160+ """Set the JSON string configuration of the merge queue.
2161+
2162+ :param config: A JSON string of configuration values.
2163+ """
2164+
2165+
2166+class IBranchMergeQueueSource(Interface):
2167+
2168+ def new(name, owner, registrant, description, configuration, branches):
2169+ """Create a new IBranchMergeQueue object.
2170+
2171+ :param name: The name of the branch merge queue.
2172+ :param description: A description of queue.
2173+ :param configuration: A JSON string of configuration values.
2174+ :param owner: The owner of the queue.
2175+ :param registrant: The registrant of the queue.
2176+ :param branches: A list of branches to add to the queue.
2177+ """
2178
2179=== added file 'lib/lp/code/model/tests/test_branchmergequeue.py'
2180--- lib/lp/code/model/tests/test_branchmergequeue.py 1970-01-01 00:00:00 +0000
2181+++ lib/lp/code/model/tests/test_branchmergequeue.py 2010-10-20 17:48:51 +0000
2182@@ -0,0 +1,155 @@
2183+# Copyright 2010 Canonical Ltd. This software is licensed under the
2184+# GNU Affero General Public License version 3 (see the file LICENSE).
2185+
2186+"""Unit tests for methods of BranchMergeQueue."""
2187+
2188+from __future__ import with_statement
2189+
2190+import simplejson
2191+
2192+import transaction
2193+
2194+from canonical.launchpad.interfaces.lpstorm import IStore
2195+from canonical.launchpad.webapp.testing import verifyObject
2196+from canonical.testing.layers import (
2197+ AppServerLayer,
2198+ DatabaseFunctionalLayer,
2199+ )
2200+from lp.code.errors import InvalidMergeQueueConfig
2201+from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
2202+from lp.code.model.branchmergequeue import BranchMergeQueue
2203+from lp.testing import (
2204+ ANONYMOUS,
2205+ person_logged_in,
2206+ launchpadlib_for,
2207+ TestCaseWithFactory,
2208+ ws_object,
2209+ )
2210+
2211+
2212+class TestBranchMergeQueueInterface(TestCaseWithFactory):
2213+ """Test IBranchMergeQueue interface."""
2214+
2215+ layer = DatabaseFunctionalLayer
2216+
2217+ def test_implements_interface(self):
2218+ queue = self.factory.makeBranchMergeQueue()
2219+ IStore(BranchMergeQueue).add(queue)
2220+ verifyObject(IBranchMergeQueue, queue)
2221+
2222+
2223+class TestBranchMergeQueueSource(TestCaseWithFactory):
2224+ """Test the methods of IBranchMergeQueueSource."""
2225+
2226+ layer = DatabaseFunctionalLayer
2227+
2228+ def test_new(self):
2229+ owner = self.factory.makePerson()
2230+ name = u'SooperQueue'
2231+ description = u'This is Sooper Queue'
2232+ config = unicode(simplejson.dumps({'test': 'make check'}))
2233+
2234+ queue = BranchMergeQueue.new(
2235+ name, owner, owner, description, config)
2236+
2237+ self.assertEqual(queue.name, name)
2238+ self.assertEqual(queue.owner, owner)
2239+ self.assertEqual(queue.registrant, owner)
2240+ self.assertEqual(queue.description, description)
2241+ self.assertEqual(queue.configuration, config)
2242+
2243+
2244+class TestBranchMergeQueue(TestCaseWithFactory):
2245+ """Test the functions of the BranchMergeQueue."""
2246+
2247+ layer = DatabaseFunctionalLayer
2248+
2249+ def test_branches(self):
2250+ """Test that a merge queue can get all its managed branches."""
2251+ store = IStore(BranchMergeQueue)
2252+
2253+ queue = self.factory.makeBranchMergeQueue()
2254+ store.add(queue)
2255+
2256+ branch = self.factory.makeBranch()
2257+ store.add(branch)
2258+ with person_logged_in(branch.owner):
2259+ branch.addToQueue(queue)
2260+
2261+ self.assertEqual(
2262+ list(queue.branches),
2263+ [branch])
2264+
2265+ def test_setMergeQueueConfig(self):
2266+ """Test that the configuration is set properly."""
2267+ queue = self.factory.makeBranchMergeQueue()
2268+ config = unicode(simplejson.dumps({
2269+ 'test': 'make test'}))
2270+
2271+ queue.setMergeQueueConfig(config)
2272+
2273+ self.assertEqual(queue.configuration, config)
2274+
2275+ def test_setMergeQueueConfig_invalid_json(self):
2276+ """Test that invalid json can't be set as the config."""
2277+ queue = self.factory.makeBranchMergeQueue()
2278+ self.assertRaises(
2279+ InvalidMergeQueueConfig,
2280+ queue.setMergeQueueConfig,
2281+ 'abc')
2282+
2283+
2284+class TestWebservice(TestCaseWithFactory):
2285+
2286+ layer = AppServerLayer
2287+
2288+ def test_properties(self):
2289+ """Test that the correct properties are exposed."""
2290+ with person_logged_in(ANONYMOUS):
2291+ name = u'teh-queue'
2292+ description = u'Oh hai! I are a queues'
2293+ configuration = unicode(simplejson.dumps({'test': 'make check'}))
2294+
2295+ queuer = self.factory.makePerson()
2296+ db_queue = self.factory.makeBranchMergeQueue(
2297+ registrant=queuer, owner=queuer, name=name,
2298+ description=description,
2299+ configuration=configuration)
2300+ branch1 = self.factory.makeBranch()
2301+ with person_logged_in(branch1.owner):
2302+ branch1.addToQueue(db_queue)
2303+ branch2 = self.factory.makeBranch()
2304+ with person_logged_in(branch2.owner):
2305+ branch2.addToQueue(db_queue)
2306+ launchpad = launchpadlib_for('test', db_queue.owner,
2307+ service_root="http://api.launchpad.dev:8085")
2308+ transaction.commit()
2309+
2310+ queuer = ws_object(launchpad, queuer)
2311+ queue = ws_object(launchpad, db_queue)
2312+ branch1 = ws_object(launchpad, branch1)
2313+ branch2 = ws_object(launchpad, branch2)
2314+
2315+ self.assertEqual(queue.registrant, queuer)
2316+ self.assertEqual(queue.owner, queuer)
2317+ self.assertEqual(queue.name, name)
2318+ self.assertEqual(queue.description, description)
2319+ self.assertEqual(queue.configuration, configuration)
2320+ self.assertEqual(queue.date_created, db_queue.date_created)
2321+ self.assertEqual(len(queue.branches), 2)
2322+
2323+ def test_set_configuration(self):
2324+ """Test the mutator for setting configuration."""
2325+ with person_logged_in(ANONYMOUS):
2326+ db_queue = self.factory.makeBranchMergeQueue()
2327+ launchpad = launchpadlib_for('test', db_queue.owner,
2328+ service_root="http://api.launchpad.dev:8085")
2329+
2330+ configuration = simplejson.dumps({'test': 'make check'})
2331+
2332+ queue = ws_object(launchpad, db_queue)
2333+ queue.configuration = configuration
2334+ queue.lp_save()
2335+
2336+ queue2 = ws_object(launchpad, db_queue)
2337+ self.assertEqual(queue2.configuration, configuration)
2338
2339=== modified file 'lib/lp/registry/doc/structural-subscriptions.txt'
2340--- lib/lp/registry/doc/structural-subscriptions.txt 2010-10-22 13:43:50 +0000
2341+++ lib/lp/registry/doc/structural-subscriptions.txt 2010-10-25 11:13:20 +0000
2342@@ -86,11 +86,7 @@
2343 When notifying subscribers of bug activity, both subscribers to the
2344 target and to the target's parent are notified.
2345
2346- >>> from canonical.launchpad.ftests import syncUpdate
2347 >>> from lp.registry.enum import BugNotificationLevel
2348- >>> from lp.registry.interfaces.structuralsubscription import BlueprintNotificationLevel
2349- >>> from lp.bugs.mail.bugnotificationrecipients import (
2350- ... BugNotificationRecipients)
2351
2352 We define some utility functions for printing out bug subscriptions and
2353 the recipients for the notifications they generate.
2354@@ -114,89 +110,6 @@
2355 >>> ubuntu_sub.bug_notification_level = BugNotificationLevel.COMMENTS
2356 >>> evolution_sub.bug_notification_level = BugNotificationLevel.COMMENTS
2357
2358-`getBugNotificationsRecipients` returns all the bug subscribers to the
2359-target and its parent, and adds the rationale for the subscriptions to
2360-the recipients set. Each subscriber is only added once.
2361-
2362- >>> recipients = BugNotificationRecipients()
2363- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
2364- ... recipients=recipients)
2365- >>> print_bug_subscriptions(ubuntu.bug_subscriptions)
2366- name12
2367- >>> print_bug_subscriptions(evolution_package.bug_subscriptions)
2368- name12
2369- >>> print_bug_subscribers(bug_subscribers)
2370- name12
2371- >>> print_bug_recipients(recipients)
2372- name12 "Subscriber (evolution in ubuntu)"
2373-
2374-Foo Bar subscribes to Ubuntu.
2375-
2376- >>> login('foo.bar@canonical.com')
2377- >>> foobar_subscription = ubuntu.addBugSubscription(foobar, foobar)
2378- >>> recipients = BugNotificationRecipients()
2379-
2380-The set of subscribers to the evolution package for ubuntu now includes
2381-both subscribers to the package, and subscribers to the distribution.
2382-
2383- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
2384- ... recipients=recipients)
2385- >>> print_bug_recipients(recipients)
2386- name16 "Subscriber (Ubuntu)"
2387- name12 "Subscriber (evolution in ubuntu)"
2388-
2389-We can pass the parameter `level` to getBugNotificationsRecipients().
2390-Subscribers whose subscription level is lower than the given parameter
2391-are not returned.
2392-
2393- >>> foobar_subscription.bug_notification_level = (
2394- ... BugNotificationLevel.METADATA)
2395- >>> recipients = BugNotificationRecipients()
2396- >>> bug_subscribers = evolution_package.getBugNotificationsRecipients(
2397- ... recipients=recipients, level=BugNotificationLevel.COMMENTS)
2398- >>> print_bug_recipients(recipients)
2399- name12 "Subscriber (evolution in ubuntu)"
2400-
2401-We remove Sample Person's bug subscription to the package.
2402-
2403- >>> evolution_sub.blueprint_notification_level = (
2404- ... BlueprintNotificationLevel.METADATA)
2405- >>> evolution_package.removeBugSubscription(sampleperson, sampleperson)
2406- >>> ubuntu.removeBugSubscription(sampleperson, sampleperson)
2407- >>> syncUpdate(evolution_sub)
2408-
2409-Sample Person is no longer a subscriber to the package, but Foo Bar
2410-is still a subscriber, by being subscribed to Ubuntu.
2411-
2412- >>> print_bug_subscribers(
2413- ... evolution_package.getBugNotificationsRecipients(
2414- ... recipients=recipients))
2415- name16
2416-
2417-A project is the parent of each of its products.
2418-
2419-Fireox does not have any subscribers.
2420-
2421- >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
2422-
2423-Mozilla is the parent of Fireox.
2424-
2425- >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
2426- >>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
2427- >>> print firefox.parent_subscription_target.displayname
2428- the Mozilla Project
2429-
2430-Foobar subscribes to bug notificatios for Mozilla.
2431-
2432- >>> mozilla.addBugSubscription(foobar, foobar)
2433- <StructuralSubscription at ...>
2434-
2435-As a result of subscribing to Mozilla, Foobar is now a subscriber of
2436-Firefox.
2437-
2438- >>> print_bug_subscribers(firefox.getBugNotificationsRecipients())
2439- name16
2440-
2441
2442 Target type display
2443 ===================
2444
2445=== modified file 'lib/lp/registry/interfaces/structuralsubscription.py'
2446--- lib/lp/registry/interfaces/structuralsubscription.py 2010-10-07 10:06:55 +0000
2447+++ lib/lp/registry/interfaces/structuralsubscription.py 2010-10-25 11:13:20 +0000
2448@@ -181,20 +181,6 @@
2449 def getSubscription(person):
2450 """Return the subscription for `person`, if it exists."""
2451
2452- def getBugNotificationsRecipients(recipients=None, level=None):
2453- """Return the set of bug subscribers to this target.
2454-
2455- :param recipients: If recipients is not None, a rationale
2456- is added for each subscriber.
2457- :type recipients: `INotificationRecipientSet`
2458- 'param level: If level is not None, only strucutral
2459- subscribers with a subscrition level greater or equal
2460- to the given value are returned.
2461- :type level: `BugNotificationLevel`
2462- :return: An `INotificationRecipientSet` instance containing
2463- the bug subscribers.
2464- """
2465-
2466 target_type_display = Attribute("The type of the target, for display.")
2467
2468 def userHasBugSubscriptions(user):
2469
2470=== modified file 'lib/lp/registry/model/structuralsubscription.py'
2471--- lib/lp/registry/model/structuralsubscription.py 2010-10-07 10:06:55 +0000
2472+++ lib/lp/registry/model/structuralsubscription.py 2010-10-25 11:13:20 +0000
2473@@ -448,24 +448,6 @@
2474 return StructuralSubscription.select(
2475 query, orderBy='Person.displayname', clauseTables=['Person'])
2476
2477- def getBugNotificationsRecipients(self, recipients=None, level=None):
2478- """See `IStructuralSubscriptionTarget`."""
2479- if level is None:
2480- subscriptions = self.bug_subscriptions
2481- else:
2482- subscriptions = self.getSubscriptions(
2483- min_bug_notification_level=level)
2484- subscribers = set(
2485- subscription.subscriber for subscription in subscriptions)
2486- if recipients is not None:
2487- for subscriber in subscribers:
2488- recipients.addStructuralSubscriber(subscriber, self)
2489- parent = self.parent_subscription_target
2490- if parent is not None:
2491- subscribers.update(
2492- parent.getBugNotificationsRecipients(recipients, level))
2493- return subscribers
2494-
2495 @property
2496 def bug_subscriptions(self):
2497 """See `IStructuralSubscriptionTarget`."""

Subscribers

People subscribed via source and target branches

to status/vote changes: