Merge lp:~brian-murray/launchpad/bug-supervisor-nominate-for-release into lp:launchpad

Proposed by Brian Murray
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11774
Proposed branch: lp:~brian-murray/launchpad/bug-supervisor-nominate-for-release
Merge into: lp:launchpad
Diff against target: 839 lines (+351/-124)
17 files modified
lib/canonical/launchpad/interfaces/ftests/validation.txt (+6/-2)
lib/canonical/launchpad/security.py (+24/-0)
lib/lp/bugs/browser/bug.py (+6/-3)
lib/lp/bugs/browser/tests/bug-nomination-views.txt (+22/-12)
lib/lp/bugs/browser/tests/bug-views.txt (+20/-6)
lib/lp/bugs/configure.zcml (+20/-20)
lib/lp/bugs/doc/bug-nomination.txt (+27/-14)
lib/lp/bugs/model/bug.py (+7/-0)
lib/lp/bugs/stories/bug-release-management/30-nominate-bug-for-distrorelease.txt (+45/-29)
lib/lp/bugs/stories/bug-release-management/40-nominate-bug-for-productseries.txt (+46/-33)
lib/lp/bugs/stories/bug-release-management/60-defer-product-bug.txt (+7/-1)
lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt (+1/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+6/-0)
lib/lp/bugs/tests/bugs-emailinterface.txt (+23/-0)
lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py (+85/-0)
lib/lp/registry/interfaces/distribution.py (+1/-1)
lib/lp/testing/factory.py (+5/-1)
To merge this branch: bzr merge lp:~brian-murray/launchpad/bug-supervisor-nominate-for-release
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+38733@code.launchpad.net

Commit message

Restrict the ability to nominate bugs for a release to bug supervisors, owners or drivers.

Description of the change

Bug 114766 is about making bug nominations more useful, for nomination reviewers, by restricting the number ability to nominate bugs for a release from anyone to the bug supervisor group. This is also something that the Ubuntu community is looking for as noted in bug 174375. I've setup the branch so the that only projects or distributions that have a bug supervisor set are affected by this change.

lib/lp/bugs/browser/bug.py:
   restrict the enabling of the +nominate link to the bug supervisor team if set
lib/lp/bugs/model/bug.py:
    added check to ensure that nominator is a member of the correct team
lib/lp/bugs/configure.zcml:
    sorted permission attributes alphabetically
lib/lp/testing/factorypy:
     add in a bug supervisor parameter to makeDistribution (one already exists for makeProject)
lib/lp/bugs/stories/bug-release-management/:
    modified tests 30- and 40- not to use sample data
    60- added in a nomination that used to be in 30- or 40-

 I added in test_bugsupervisor_bugnomination.py:
    bin/test -cvvt test_bugsupervisor_bugnomination
I've also modified the tests in bug-release-management:
    bin/test -cvvt bug-release-management

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Per IRC discussion, please change this so that the permission is only provided for bug supervisors.

review: Needs Fixing
Revision history for this message
Brian Murray (brian-murray) wrote :

I've updated the branch per our discussions so that only bug supervisors, drivers or owners see the +nominate link and can nominate a bug for a release. I also created 2 more permission classes NominateBugForProductSeries and NominateBugForDistroSeries. Also due to the changes in not allowing just anyone to nominate a bug for a release I had to modify a couple of tests (bug-nomination-views.txt and bug-nomination.txt).

Revision history for this message
Aaron Bentley (abentley) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/ftests/validation.txt'
2--- lib/canonical/launchpad/interfaces/ftests/validation.txt 2010-10-18 22:24:59 +0000
3+++ lib/canonical/launchpad/interfaces/ftests/validation.txt 2010-10-21 01:54:46 +0000
4@@ -1,10 +1,16 @@
5 = Launchpad field validators =
6
7+ >>> from zope.security.proxy import removeSecurityProxy
8 >>> from zope.component import getUtility
9 >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
10 >>> from lp.bugs.interfaces.bug import CreateBugParams
11 >>> from lp.registry.interfaces.person import IPersonSet
12 >>> from lp.registry.interfaces.product import IProductSet
13+ >>> no_priv = getUtility(IPersonSet).getByEmail(
14+ ... 'no-priv@canonical.com')
15+ >>> firefox = getUtility(IProductSet).getByName("firefox")
16+ >>> firefox = removeSecurityProxy(firefox)
17+ >>> firefox.bug_supervisor = no_priv
18
19 == can_be_nominated_for_series ==
20
21@@ -16,8 +22,6 @@
22 If we create a new bug, all the target's series can be nominated.
23
24 >>> login('no-priv@canonical.com')
25- >>> no_priv = getUtility(IPersonSet).getByEmail(
26- ... 'no-priv@canonical.com')
27 >>> firefox = getUtility(IProductSet).getByName('firefox')
28 >>> bug = firefox.createBug(
29 ... CreateBugParams(no_priv, "New Bug", comment="New Bug."))
30
31=== modified file 'lib/canonical/launchpad/security.py'
32--- lib/canonical/launchpad/security.py 2010-10-14 21:47:33 +0000
33+++ lib/canonical/launchpad/security.py 2010-10-21 01:54:46 +0000
34@@ -913,6 +913,30 @@
35 user.in_admin)
36
37
38+class NominateBugForProductSeries(AuthorizationBase):
39+ """Product's owners and bug supervisors can add bug nominations."""
40+
41+ permission = 'launchpad.BugSupervisor'
42+ usedfor = IProductSeries
43+
44+ def checkAuthenticated(self, user):
45+ return (user.inTeam(self.obj.product.bug_supervisor) or
46+ user.inTeam(self.obj.product.owner) or
47+ user.in_admin)
48+
49+
50+class NominateBugForDistroSeries(AuthorizationBase):
51+ """Distro's owners and bug supervisors can add bug nominations."""
52+
53+ permission = 'launchpad.BugSupervisor'
54+ usedfor = IDistroSeries
55+
56+ def checkAuthenticated(self, user):
57+ return (user.inTeam(self.obj.distribution.bug_supervisor) or
58+ user.inTeam(self.obj.distribution.owner) or
59+ user.in_admin)
60+
61+
62 class AdminDistroSeries(AdminByAdminsTeam):
63 """Soyuz involves huge chunks of data in the archive and librarian,
64 so for the moment we are locking down admin and edit on distributions
65
66=== modified file 'lib/lp/bugs/browser/bug.py'
67--- lib/lp/bugs/browser/bug.py 2010-09-23 10:27:11 +0000
68+++ lib/lp/bugs/browser/bug.py 2010-10-21 01:54:46 +0000
69@@ -273,10 +273,13 @@
70 target = launchbag.product or launchbag.distribution
71 if check_permission("launchpad.Driver", target):
72 text = "Target to release"
73- else:
74+ return Link('+nominate', text, icon='milestone')
75+ elif (check_permission("launchpad.BugSupervisor", target) or
76+ self.user is None):
77 text = 'Nominate for release'
78-
79- return Link('+nominate', text, icon='milestone')
80+ return Link('+nominate', text, icon='milestone')
81+ else:
82+ return Link('+nominate', '', enabled=False, icon='milestone')
83
84 def addcomment(self):
85 """Return the 'Comment or attach file' Link."""
86
87=== modified file 'lib/lp/bugs/browser/tests/bug-nomination-views.txt'
88--- lib/lp/bugs/browser/tests/bug-nomination-views.txt 2010-10-18 22:24:59 +0000
89+++ lib/lp/bugs/browser/tests/bug-nomination-views.txt 2010-10-21 01:54:46 +0000
90@@ -1,18 +1,33 @@
91 = Bug Nomination Pages =
92
93 Series targeting is done on the +nominate page of a bug. From here,
94-normal users can propose that the bug be fixed in specific distribution
95+bug supervisors can propose that the bug be fixed in specific distribution
96 and product series, and release managers can directly target
97 the bug to series for which they are drivers.
98
99+ >>> from zope.security.proxy import removeSecurityProxy
100 >>> from zope.component import getUtility, getMultiAdapter
101
102+ >>> from canonical.launchpad.ftests import login_person
103 >>> from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
104 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
105 >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
106-
107- >>> login("no-priv@canonical.com")
108-
109+ >>> from lp.registry.interfaces.distribution import IDistributionSet
110+ >>> from lp.registry.interfaces.person import IPersonSet
111+ >>> from lp.registry.interfaces.product import IProductSet
112+ >>> from lp.testing.sampledata import (ADMIN_EMAIL)
113+
114+ >>> login(ADMIN_EMAIL)
115+ >>> nominator = factory.makePerson(name='nominator')
116+ >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
117+ >>> ubuntu = removeSecurityProxy(ubuntu)
118+ >>> ubuntu.bug_supervisor = nominator
119+ >>> firefox = getUtility(IProductSet).getByName("firefox")
120+ >>> firefox = removeSecurityProxy(firefox)
121+ >>> firefox.bug_supervisor = nominator
122+ >>> logout()
123+
124+ >>> login_person(nominator)
125 >>> request = LaunchpadTestRequest()
126 >>> bug_one_in_ubuntu_firefox = getUtility(IBugTaskSet).get(17)
127 >>> print bug_one_in_ubuntu_firefox.bug.id
128@@ -32,7 +47,7 @@
129
130 Here's an example of nominating a bug for a distroseries.
131
132- >>> login("no-priv@canonical.com")
133+ >>> login_person(nominator)
134
135 >>> request = LaunchpadTestRequest(
136 ... method="POST",
137@@ -44,13 +59,10 @@
138
139 (Add objects to the LaunchBag that will be used by the view.)
140
141- >>> from lp.registry.interfaces.distribution import IDistributionSet
142-
143 >>> launchbag.clear()
144 >>> launchbag.add(bug_one_in_ubuntu_firefox)
145 >>> launchbag.add(bug_one_in_ubuntu_firefox.distribution)
146
147- >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
148 >>> ubuntu_warty = ubuntu.getSeries("warty")
149
150 >>> bug_one = bug_one_in_ubuntu_firefox.bug
151@@ -167,11 +179,11 @@
152
153 The batch navigator used returns a BugTaskListingItem for each of the
154 distribution's bugtasks that are nominated for this release, with a
155-widget set up for accepting/declining the nomination. The widet is only
156+widget set up for accepting/declining the nomination. The widget is only
157 set up if the user has permission to approve the nomination.
158
159 >>> from lp.bugs.interfaces.bug import CreateBugParams
160- >>> login("no-priv@canonical.com")
161+ >>> login_person(nominator)
162 >>> no_priv = getUtility(ILaunchBag).user
163 >>> ubuntu_bug = ubuntu.createBug(CreateBugParams(
164 ... no_priv, "Ubuntu bug", comment="Test bug."))
165@@ -228,8 +240,6 @@
166 context, we mean a user that has, either directly or through a team,
167 launchpad.Driver permission on the nomination.
168
169- >>> from lp.registry.interfaces.person import IPersonSet
170-
171 >>> ubuntu_team = getUtility(IPersonSet).getByName("ubuntu-team")
172
173 >>> login("celso.providelo@canonical.com")
174
175=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
176--- lib/lp/bugs/browser/tests/bug-views.txt 2010-10-19 18:44:31 +0000
177+++ lib/lp/bugs/browser/tests/bug-views.txt 2010-10-21 01:54:46 +0000
178@@ -482,12 +482,26 @@
179 bugtask, New, mozilla-firefox (Ubuntu)
180 nomination, Nominated, Ubuntu Hoary
181
182-Let's nominate the bug for upstream and an Ubuntu series and see how
183-the list changes.
184-
185-(Login as an unprivileged user to be able to nominate.)
186-
187- >>> login("no-priv@canonical.com")
188+After creating bug supervisors for Ubuntu and Firefox Let's nominate the bug
189+for upstream and an Ubuntu series and see how the list changes.
190+
191+ >>> from lp.testing.sampledata import (ADMIN_EMAIL)
192+ >>> from zope.component import getUtility
193+ >>> from zope.security.proxy import removeSecurityProxy
194+ >>>
195+ >>> login(ADMIN_EMAIL)
196+ >>> nominator = factory.makePerson(name='nominator')
197+ >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
198+ >>> ubuntu = removeSecurityProxy(ubuntu)
199+ >>> ubuntu.bug_supervisor = nominator
200+ >>> firefox = getUtility(IProductSet).getByName("firefox")
201+ >>> firefox = removeSecurityProxy(firefox)
202+ >>> firefox.bug_supervisor = nominator
203+ >>> logout()
204+
205+(Login as a bug supervisor to be able to nominate.)
206+
207+ >>> login_person(nominator)
208
209 >>> current_user = getUtility(ILaunchBag).user
210 >>> ubuntu_warty = ubuntu.getSeries("warty")
211
212=== modified file 'lib/lp/bugs/configure.zcml'
213--- lib/lp/bugs/configure.zcml 2010-10-18 01:06:18 +0000
214+++ lib/lp/bugs/configure.zcml 2010-10-21 01:54:46 +0000
215@@ -731,35 +731,35 @@
216 <require
217 permission="launchpad.Edit"
218 attributes="
219+ addAttachment
220+ addChange
221 addChangeNotification
222 addCommentNotification
223+ addNomination
224+ addTask
225 addWatch
226- removeWatch
227+ convertToQuestion
228+ expireNotifications
229+ findCvesInText
230+ linkAttachment
231+ linkBranch
232 linkCVE
233 linkCVEAndReturnNothing
234- unlinkCVE
235- findCvesInText
236+ linkHWSubmission
237+ linkMessage
238+ markAsDuplicate
239+ markUserAffected
240 newMessage
241- linkMessage
242- addAttachment
243- unsubscribeFromDupes
244- subscribe
245- unsubscribe
246- addNomination
247- expireNotifications
248- setStatus
249+ removeWatch
250 setPrivate
251 setSecurityRelated
252- convertToQuestion
253- markUserAffected
254- addTask
255- addChange
256- markAsDuplicate
257- linkHWSubmission
258+ setStatus
259+ subscribe
260+ unlinkBranch
261+ unlinkCVE
262 unlinkHWSubmission
263- linkBranch
264- unlinkBranch
265- linkAttachment
266+ unsubscribe
267+ unsubscribeFromDupes
268 updateHeat"
269 set_attributes="
270 activity initial_message
271
272=== modified file 'lib/lp/bugs/doc/bug-nomination.txt'
273--- lib/lp/bugs/doc/bug-nomination.txt 2010-10-18 22:24:59 +0000
274+++ lib/lp/bugs/doc/bug-nomination.txt 2010-10-21 01:54:46 +0000
275@@ -1,18 +1,31 @@
276 = Bug Nomination =
277
278-Any logged-in user can nominate a bug to be fixed in a specific
279+A bug supervisor can nominate a bug to be fixed in a specific
280 distribution or product series. Nominations are created by
281 calling IBug.addNomination.
282
283 >>> from zope.component import getUtility
284 >>> from zope.interface.verify import verifyClass
285+ >>> from zope.security.proxy import removeSecurityProxy
286+ >>> from canonical.launchpad.ftests import login_person
287 >>> from lp.bugs.interfaces.bug import IBugSet
288 >>> from lp.bugs.interfaces.bugnomination import IBugNomination
289 >>> from lp.bugs.model.bugnomination import BugNomination
290 >>> from lp.registry.interfaces.distribution import IDistributionSet
291 >>> from lp.registry.interfaces.person import IPersonSet
292+ >>> from lp.registry.interfaces.product import IProductSet
293+ >>> from lp.testing.sampledata import (ADMIN_EMAIL)
294+ >>> login(ADMIN_EMAIL)
295+ >>> nominator = factory.makePerson(name='nominator')
296+ >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
297+ >>> ubuntu = removeSecurityProxy(ubuntu)
298+ >>> ubuntu.bug_supervisor = nominator
299+ >>> firefox = getUtility(IProductSet).getByName("firefox")
300+ >>> firefox = removeSecurityProxy(firefox)
301+ >>> firefox.bug_supervisor = nominator
302+ >>> logout()
303
304- >>> login("no-priv@canonical.com")
305+ >>> login_person(nominator)
306
307 The BugNomination class implements IBugNomination.
308
309@@ -22,19 +35,18 @@
310 >>> bugset = getUtility(IBugSet)
311 >>> bug_one = bugset.get(1)
312
313- >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
314 >>> ubuntu_grumpy = ubuntu.getSeries("grumpy")
315 >>> personset = getUtility(IPersonSet)
316- >>> no_privs = personset.getByName("no-priv")
317+ >>> nominator = personset.getByName("nominator")
318
319 >>> grumpy_nomination = bug_one.addNomination(
320- ... target=ubuntu_grumpy, owner=no_privs)
321+ ... target=ubuntu_grumpy, owner=nominator)
322
323 The nomination records the distro series or series for which the bug
324-was nominated and the user that submitted the nomination (the "owner".)
325+was nominated and the user that submitted the nomination (the "owner").
326
327 >>> print grumpy_nomination.owner.name
328- no-priv
329+ nominator
330
331 >>> print grumpy_nomination.distroseries.fullseriesname
332 Ubuntu Grumpy
333@@ -47,13 +59,13 @@
334
335 >>> firefox_trunk = firefox.getSeries("trunk")
336
337- >>> no_privs = personset.getByName("no-priv")
338+ >>> nominator = personset.getByName("nominator")
339
340 >>> firefox_ms_nomination = bug_one.addNomination(
341- ... target=firefox_trunk, owner=no_privs)
342+ ... target=firefox_trunk, owner=nominator)
343
344 >>> print firefox_ms_nomination.owner.name
345- no-priv
346+ nominator
347
348 >>> print firefox_ms_nomination.productseries.title
349 Mozilla Firefox trunk series
350@@ -149,7 +161,7 @@
351 as "Nominated".
352
353 >>> ubuntu_breezy_autotest_nomination = bug_one.addNomination(
354- ... target=ubuntu_breezy_autotest, owner=no_privs)
355+ ... target=ubuntu_breezy_autotest, owner=nominator)
356
357 >>> print ubuntu_breezy_autotest_nomination.status.title
358 Nominated
359@@ -179,17 +191,17 @@
360
361 >>> current_user = getUtility(ILaunchBag).user
362
363- >>> current_user == no_privs
364+ >>> current_user == nominator
365 True
366 >>> check_permission("launchpad.Driver", firefox_ms_nomination)
367 False
368
369- >>> firefox_ms_nomination.approve(no_privs)
370+ >>> firefox_ms_nomination.approve(nominator)
371 Traceback (most recent call last):
372 ..
373 Unauthorized: ...
374
375- >>> firefox_ms_nomination.decline(no_privs)
376+ >>> firefox_ms_nomination.decline(nominator)
377 Traceback (most recent call last):
378 ..
379 Unauthorized: ...
380@@ -198,6 +210,7 @@
381
382 >>> login("foo.bar@canonical.com")
383
384+ >>> no_privs = personset.getByName("no-priv")
385 >>> firefox_ms_nomination.target.driver = no_privs
386
387 >>> login("no-priv@canonical.com")
388
389=== modified file 'lib/lp/bugs/model/bug.py'
390--- lib/lp/bugs/model/bug.py 2010-10-15 16:09:18 +0000
391+++ lib/lp/bugs/model/bug.py 2010-10-21 01:54:46 +0000
392@@ -101,6 +101,7 @@
393 IndexedMessage,
394 )
395 from canonical.launchpad.validators import LaunchpadValidationError
396+from canonical.launchpad.webapp.authorization import check_permission
397 from canonical.launchpad.webapp.interfaces import (
398 DEFAULT_FLAVOR,
399 IStoreSelector,
400@@ -1540,6 +1541,12 @@
401 assert IProductSeries.providedBy(target)
402 productseries = target
403
404+ admins = getUtility(ILaunchpadCelebrities).admin
405+ if not (check_permission("launchpad.BugSupervisor", target) or
406+ check_permission("launchpad.Driver", target)):
407+ raise NominationError(
408+ "Only bug supervisors or owners can nominate bugs.")
409+
410 nomination = BugNomination(
411 owner=owner, bug=self, distroseries=distroseries,
412 productseries=productseries)
413
414=== modified file 'lib/lp/bugs/stories/bug-release-management/30-nominate-bug-for-distrorelease.txt'
415--- lib/lp/bugs/stories/bug-release-management/30-nominate-bug-for-distrorelease.txt 2009-06-12 16:36:02 +0000
416+++ lib/lp/bugs/stories/bug-release-management/30-nominate-bug-for-distrorelease.txt 2010-10-21 01:54:46 +0000
417@@ -2,51 +2,67 @@
418
419 A bug can be nominated for a distribution release.
420
421- >>> user_browser.open(
422- ... "http://launchpad.dev/distros/ubuntu/+source/mozilla-firefox/"
423- ... "+bug/1/+nominate")
424+ >>> login('foo.bar@canonical.com')
425+ >>> nominater = factory.makePerson(name='denominater',
426+ ... password='g00dpassword')
427+ >>> poseidon = factory.makeDistribution(name='poseidon',
428+ ... bug_supervisor=nominater)
429+ >>> dsp = factory.makeDistributionSourcePackage(distribution=poseidon)
430+ >>> series = factory.makeDistroSeries(distribution=poseidon,
431+ ... name='aqua')
432+ >>> series = factory.makeDistroSeries(distribution=poseidon,
433+ ... name='hydro')
434+ >>> bug_task = factory.makeBugTask(target=dsp)
435+ >>> nominater_browser = setupBrowser(
436+ ... auth='Basic %s:g00dpassword' %
437+ ... nominater.preferredemail.email)
438+ >>> logout()
439+ >>> nominater_browser.open(
440+ ... "http://launchpad.dev/poseidon/+source/%s/+bug/%s/+nominate" %
441+ ... (dsp.name, bug_task.bug.id))
442
443 Before we continue, we'll set up a second browser instance, to simulate
444-no-priv accessing the site from another window. Working with the same
445-form in different browser windows or tabs can sometimes trigger edge
446-case errors, and we'll give an example of one shortly.
447-
448- >>> nopriv_other_browser = setupBrowser(
449- ... auth="Basic no-priv@canonical.com:test")
450- >>> nopriv_other_browser.open(
451- ... "http://launchpad.dev/distros/ubuntu/+source/mozilla-firefox/"
452- ... "+bug/1/+nominate")
453-
454- >>> user_browser.getControl("Grumpy").selected = True
455- >>> nopriv_other_browser.getControl("Grumpy").selected = True
456-
457- >>> user_browser.getControl("Submit").click()
458-
459- >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
460+the nominater accessing the site from another window. Working with the same
461+form in different browser windows or tabs can sometimes trigger edge case
462+errors, and we'll give an example of one shortly.
463+
464+ >>> login('foo.bar@canonical.com')
465+ >>> nominater_other_browser = setupBrowser(
466+ ... auth='Basic %s:g00dpassword' %
467+ ... nominater.preferredemail.email)
468+ >>> logout()
469+ >>> nominater_other_browser.open(
470+ ... "http://launchpad.dev/poseidon/+source/%s/+bug/%s/+nominate" %
471+ ... (dsp.name, bug_task.bug.id))
472+ >>> nominater_browser.getControl("Aqua").selected = True
473+ >>> nominater_browser.getControl("Submit").click()
474+ >>> for tag in find_tags_by_class(nominater_browser.contents, 'message'):
475 ... print tag
476- <div...Added nominations for: Ubuntu Grumpy...
477+ <div...Added nominations for: Poseidon Aqua...
478
479-Now, if no-priv, having the form open in another browser window,
480-accidentally nominates the bug for Grumpy a second time, an error is
481+Now, if the nominater, having the form open in another browser window,
482+accidentally nominates the bug for Aqua a second time, an error is
483 raised.
484
485- >>> nopriv_other_browser.getControl("Submit").click()
486+ >>> nominater_other_browser.getControl("Aqua").selected = True
487+ >>> nominater_other_browser.getControl("Submit").click()
488
489- >>> for tag in find_tags_by_class(nopriv_other_browser.contents, 'message'):
490+ >>> for tag in find_tags_by_class(nominater_other_browser.contents,
491+ ... 'message'):
492 ... print tag.renderContents()
493 There is 1 error.
494- This bug has already been nominated for these series: Grumpy
495+ This bug has already been nominated for these series: Aqua
496
497 When a nomination is submitted by a privileged user, it is immediately
498 approved and targeted to the release.
499
500 >>> admin_browser.open(
501- ... "http://launchpad.dev/distros/ubuntu/+source/mozilla-firefox/"
502- ... "+bug/1/+nominate")
503+ ... "http://launchpad.dev/poseidon/+source/%s/+bug/%s/+nominate" %
504+ ... (dsp.name, bug_task.bug.id))
505
506- >>> admin_browser.getControl("Warty").selected = True
507+ >>> admin_browser.getControl("Hydro").selected = True
508 >>> admin_browser.getControl("Submit").click()
509
510 >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
511 ... print tag
512- <div...Targeted bug to: Ubuntu Warty...
513+ <div...Targeted bug to: Poseidon Hydro...
514
515=== modified file 'lib/lp/bugs/stories/bug-release-management/40-nominate-bug-for-productseries.txt'
516--- lib/lp/bugs/stories/bug-release-management/40-nominate-bug-for-productseries.txt 2009-06-12 16:36:02 +0000
517+++ lib/lp/bugs/stories/bug-release-management/40-nominate-bug-for-productseries.txt 2010-10-21 01:54:46 +0000
518@@ -1,51 +1,64 @@
519-=Nominating a bug for a product series =
520+= Nominating a bug for a product series =
521
522 A bug can be nominated for a product series.
523
524- >>> user_browser.open(
525- ... "http://launchpad.dev/products/firefox/+bug/4/+nominate")
526+ >>> login('foo.bar@canonical.com')
527+ >>> nominater = factory.makePerson(name='nominater',
528+ ... password='g00dpassword')
529+ >>> widget = factory.makeProduct(name='widget',
530+ ... official_malone = True,
531+ ... bug_supervisor=nominater)
532+ >>> series = factory.makeProductSeries(product=widget,
533+ ... name='beta')
534+ >>> bug = factory.makeBug(product=widget)
535+ >>> nominater_browser = setupBrowser(
536+ ... auth='Basic %s:g00dpassword' %
537+ ... nominater.preferredemail.email)
538+ >>> logout()
539+ >>> nominater_browser.open(
540+ ... "http://launchpad.dev/widget/+bug/%s/+nominate" % bug.id)
541
542 Before we continue, we'll set up a second browser instance, to simulate
543-no-priv accessing the site from another window. Working with the same
544-form in different browser windows or tabs can sometimes trigger edge
545-case errors, and we'll give an example of one shortly.
546-
547- >>> nopriv_other_browser = setupBrowser(
548- ... auth="Basic no-priv@canonical.com:test")
549- >>> nopriv_other_browser.open(
550- ... "http://launchpad.dev/products/firefox/+bug/4/+nominate")
551-
552- >>> user_browser.getControl("1.0").selected = True
553- >>> nopriv_other_browser.getControl("1.0").selected = True
554-
555- >>> user_browser.getControl("Submit").click()
556-
557- >>> for tag in find_tags_by_class(
558- ... user_browser.contents, 'informational message'):
559+the nominater accessing the site from another window. Working with the same
560+form in different browser windows or tabs can sometimes trigger edge case
561+errors, and we'll give an example of one shortly.
562+
563+ >>> login('foo.bar@canonical.com')
564+ >>> nominater_other_browser = setupBrowser(
565+ ... auth='Basic %s:g00dpassword' %
566+ ... nominater.preferredemail.email)
567+ >>> logout()
568+ >>> nominater_other_browser.open(
569+ ... "http://launchpad.dev/widget/+bug/%s/+nominate" % bug.id)
570+
571+ >>> nominater_browser.getControl("Beta").selected = True
572+ >>> nominater_other_browser.getControl("Beta").selected = True
573+ >>> nominater_browser.getControl("Submit").click()
574+
575+ >>> for tag in find_tags_by_class(nominater_browser.contents, 'message'):
576 ... print tag
577- <div...Added nominations for: Mozilla Firefox 1.0...
578-
579-Now, if no-priv, having the form open in another browser window,
580-accidentally nominates the bug for firefox 1.0 a second time, an error
581-is raised.
582-
583- >>> nopriv_other_browser.getControl("Submit").click()
584-
585- >>> for tag in find_tags_by_class(nopriv_other_browser.contents, 'message'):
586+ <div...Added nominations for: Widget beta...
587+
588+Now, if the nominater, having the form open in another browser window,
589+accidentally nominates the bug for Beta a second time, an error is raised.
590+
591+ >>> nominater_other_browser.getControl("Submit").click()
592+
593+ >>> for tag in find_tags_by_class(nominater_other_browser.contents,
594+ ... 'message'):
595 ... print tag.renderContents()
596 There is 1 error.
597- This bug has already been nominated for these series: 1.0
598+ This bug has already been nominated for these series: Beta
599
600 When a nomination is submitted by a privileged user, it is immediately
601 approved and targeted to the release.
602
603 >>> admin_browser.open(
604- ... "http://launchpad.dev/products/firefox/+bug/4/+nominate")
605+ ... "http://launchpad.dev/widget/+bug/%s/+nominate" % bug.id)
606
607 >>> admin_browser.getControl("Trunk").selected = True
608 >>> admin_browser.getControl("Submit").click()
609
610- >>> for tag in find_tags_by_class(
611- ... admin_browser.contents, 'informational message'):
612+ >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
613 ... print tag
614- <div...Targeted bug to: Mozilla Firefox trunk...
615+ <div...Targeted bug to: Widget trunk...
616
617=== modified file 'lib/lp/bugs/stories/bug-release-management/60-defer-product-bug.txt'
618--- lib/lp/bugs/stories/bug-release-management/60-defer-product-bug.txt 2010-06-11 21:51:48 +0000
619+++ lib/lp/bugs/stories/bug-release-management/60-defer-product-bug.txt 2010-10-21 01:54:46 +0000
620@@ -2,6 +2,11 @@
621 product task is no longer editable. Instead the status is tracked
622 in the series task.
623
624+ >>> admin_browser.open(
625+ ... "http://launchpad.dev/products/firefox/+bug/4/+nominate")
626+ >>> admin_browser.getControl("Trunk").selected = True
627+ >>> admin_browser.getControl("Submit").click()
628+
629 >>> user_browser.open('http://bugs.launchpad.dev/firefox/+bug/4')
630 >>> firefox_edit_url = (
631 ... 'http://bugs.launchpad.dev/firefox/+bug/4/+editstatus')
632@@ -26,7 +31,8 @@
633 >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
634 ... print tag.renderContents()
635
636- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'bug-supervisor'))
637+ >>> print extract_text(find_tag_by_id(admin_browser.contents,
638+ ... 'bug-supervisor'))
639 Bug supervisor:
640 No Privileges Person
641
642
643=== modified file 'lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt'
644--- lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt 2009-06-12 16:36:02 +0000
645+++ lib/lp/bugs/stories/bug-release-management/xx-anonymous-bug-nomination.txt 2010-10-21 01:54:46 +0000
646@@ -8,5 +8,4 @@
647 >>> anon_browser.getLink('Nominate for release').click()
648 Traceback (most recent call last):
649 ...
650- Unauthorized:... 'launchpad.Edit'...
651-
652+ Unauthorized:...'launchpad.Edit'...
653
654=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
655--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-10-18 22:24:59 +0000
656+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-10-21 01:54:46 +0000
657@@ -631,8 +631,14 @@
658 total_size: 0
659 ---
660
661+ >>> from zope.component import getUtility
662+ >>> from zope.security.proxy import removeSecurityProxy
663 >>> login('foo.bar@canonical.com')
664 >>> john = factory.makePerson(name='john')
665+ >>> debuntu = removeSecurityProxy(debuntu)
666+ >>> debuntu.bug_supervisor = john
667+ >>> fooix = removeSecurityProxy(fooix)
668+ >>> fooix.bug_supervisor = john
669 >>> logout()
670
671 >>> from canonical.launchpad.testing.pages import webservice_for_person
672
673=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
674--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-10-18 22:24:59 +0000
675+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-10-21 01:54:46 +0000
676@@ -1134,6 +1134,22 @@
677 Only owners, drivers and bug supervisors may assign milestones.
678 ...
679
680+Sample person must be a bug supervisor for Ubuntu and Evolution to be able to
681+nominate bugs for a release.
682+
683+ >>> from lp.registry.interfaces.distribution import IDistributionSet
684+ >>> from lp.testing.sampledata import (ADMIN_EMAIL)
685+ >>> from zope.component import getUtility
686+ >>> from zope.security.proxy import removeSecurityProxy
687+ >>>
688+ >>> login(ADMIN_EMAIL)
689+ >>> sample_person = getUtility(IPersonSet).getByEmail(
690+ ... "test@canonical.com")
691+ >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
692+ >>> ubuntu = removeSecurityProxy(ubuntu)
693+ >>> ubuntu.bug_supervisor = sample_person
694+ >>> logout()
695+
696 >>> login('test@canonical.com')
697
698 Like the web UI, we can assign a bug to nobody.
699@@ -1331,6 +1347,13 @@
700 bug task will be created, only a nomination. A general product bugtask
701 will be created if one doesn't exist.
702
703+ >>> login(ADMIN_EMAIL)
704+ >>> no_priv = getUtility(IPersonSet).getByEmail("no-priv@canonical.com")
705+ >>> evolution = getUtility(IProductSet).getByName("evolution")
706+ >>> evolution = removeSecurityProxy(evolution)
707+ >>> evolution.bug_supervisor = no_priv
708+ >>> logout()
709+
710 >>> login('no-priv@canonical.com')
711 >>> bug = new_firefox_bug()
712 >>> submit_commands(bug, 'affects /evolution/trunk')
713
714=== added file 'lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py'
715--- lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 1970-01-01 00:00:00 +0000
716+++ lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 2010-10-21 01:54:46 +0000
717@@ -0,0 +1,85 @@
718+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
719+# GNU Affero General Public License version 3 (see the file LICENSE).
720+
721+"""Tests related to bug nominations for an object with a bug supervisor."""
722+
723+__metaclass__ = type
724+
725+from canonical.launchpad.ftests import (
726+ login,
727+ login_person,
728+ logout,
729+ )
730+from canonical.testing.layers import DatabaseFunctionalLayer
731+
732+from lp.bugs.interfaces.bugnomination import NominationError
733+from lp.testing import TestCaseWithFactory
734+
735+
736+class AddNominationTestMixin:
737+ """Test case mixin for IBug.addNomination."""
738+
739+ layer = DatabaseFunctionalLayer
740+
741+ def setUp(self):
742+ super(AddNominationTestMixin, self).setUp()
743+ login('foo.bar@canonical.com')
744+ self.user = self.factory.makePerson(name='ordinary-user')
745+ self.bug_supervisor = self.factory.makePerson(name='no-ordinary-user')
746+ self.owner = self.factory.makePerson(name='extraordinary-user')
747+ self.setUpTarget()
748+ logout()
749+
750+ def tearDown(self):
751+ logout()
752+ super(AddNominationTestMixin, self).tearDown()
753+
754+ def test_user_addNominationFor_series(self):
755+ # A bug may not be nominated for a series of a product with an
756+ # existing task by just anyone.
757+ login_person(self.user)
758+ self.assertRaises(NominationError,
759+ self.bug.addNomination, self.user, self.series)
760+
761+ def test_bugsupervisor_addNominationFor_series(self):
762+ # A bug may be nominated for a series of a product with an
763+ # exisiting task by the product's bug supervisor.
764+ login_person(self.bug_supervisor)
765+ self.bug.addNomination(self.bug_supervisor, self.series)
766+ self.assertTrue(len(self.bug.getNominations()), 1)
767+
768+ def test_owner_addNominationFor_series(self):
769+ # A bug may be nominated for a series of a product with an
770+ # exisiting task by the product's owner.
771+ login_person(self.owner)
772+ self.bug.addNomination(self.owner, self.series)
773+ self.assertTrue(len(self.bug.getNominations()), 1)
774+
775+
776+class TestBugAddNominationProductSeries(
777+ AddNominationTestMixin, TestCaseWithFactory):
778+ """Test IBug.addNomination for IProductSeries nominations."""
779+
780+ def setUpTarget(self):
781+ self.product = self.factory.makeProduct(official_malone = True,
782+ bug_supervisor=self.bug_supervisor,
783+ owner=self.owner)
784+ self.series = self.factory.makeProductSeries(product=self.product)
785+ self.bug = self.factory.makeBug(product=self.product)
786+ self.milestone = self.factory.makeMilestone(productseries=self.series)
787+
788+
789+class TestBugAddNominationDistroSeries(
790+ AddNominationTestMixin, TestCaseWithFactory):
791+ """Test IBug.addNomination for IDistroSeries nominations."""
792+
793+ def setUpTarget(self):
794+ self.distro = self.factory.makeDistribution(
795+ bug_supervisor=self.bug_supervisor,
796+ owner=self.owner)
797+ self.series = self.factory.makeDistroRelease(distribution=self.distro)
798+ # The factory can't create a distro bug directly.
799+ self.bug = self.factory.makeBug()
800+ self.bug.addTask(self.bug_supervisor, self.distro)
801+ self.milestone = self.factory.makeMilestone(
802+ distribution=self.distro)
803
804=== modified file 'lib/lp/registry/interfaces/distribution.py'
805--- lib/lp/registry/interfaces/distribution.py 2010-08-20 20:31:18 +0000
806+++ lib/lp/registry/interfaces/distribution.py 2010-10-21 01:54:46 +0000
807@@ -670,7 +670,7 @@
808
809 def new(name, displayname, title, description, summary, domainname,
810 members, owner, mugshot=None, logo=None, icon=None):
811- """Creaste a new distribution."""
812+ """Create a new distribution."""
813
814
815 class NoSuchDistribution(NameLookupFailed):
816
817=== modified file 'lib/lp/testing/factory.py'
818--- lib/lp/testing/factory.py 2010-10-19 22:07:04 +0000
819+++ lib/lp/testing/factory.py 2010-10-21 01:54:46 +0000
820@@ -1849,7 +1849,8 @@
821 return library_file_alias
822
823 def makeDistribution(self, name=None, displayname=None, owner=None,
824- members=None, title=None, aliases=None):
825+ members=None, title=None, aliases=None,
826+ bug_supervisor=None):
827 """Make a new distribution."""
828 if name is None:
829 name = self.getUniqueString(prefix="distribution")
830@@ -1869,6 +1870,9 @@
831 members, owner)
832 if aliases is not None:
833 removeSecurityProxy(distro).setAliases(aliases)
834+ if bug_supervisor is not None:
835+ naked_distro = removeSecurityProxy(distro)
836+ naked_distro.bug_supervisor = bug_supervisor
837 return distro
838
839 def makeDistroRelease(self, distribution=None, version=None,