Merge lp:~sinzui/launchpad/nomination-investigation-1 into lp:launchpad
- nomination-investigation-1
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Curtis Hovey |
Approved revision: | no longer in the source branch. |
Merged at revision: | 16141 |
Proposed branch: | lp:~sinzui/launchpad/nomination-investigation-1 |
Merge into: | lp:launchpad |
Diff against target: |
902 lines (+248/-573) 5 files modified
lib/lp/bugs/doc/bug-nomination.txt (+0/-570) lib/lp/bugs/interfaces/bugnomination.py (+1/-2) lib/lp/bugs/model/tests/test_bug.py (+25/-0) lib/lp/bugs/tests/test_bugnomination.py (+200/-0) lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py (+22/-1) |
To merge this branch: | bzr merge lp:~sinzui/launchpad/nomination-investigation-1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+129489@code.launchpad.net |
Commit message
Replace doc/bug-
Description of the change
Rewrite a doctest while investigating a fix timeout issue.
-------
QA
* None, this is just a test change.
LINT
lib/
lib/
lib/
lib/
TEST
./bin/test -vvc -t Nomination lp.bugs.
./bin/test -vvc lp.bugs.
./bin/test -vvc lp.bugs.
IMPLEMENTATION
Rewrote doc/bug-
About half of the doctest was already unit tested. There were three
surprises.
1. I was not aware that approving a nomination for a package approved
all the packages too. I found a bug questioning this behaviour and
mentioned it in the test's comment.
2. The "Automatic targeting of new source packages" section was a lie.
The test did not show what the narrative claimed. I think the rules
changed and the test was also changed, but it should have been deleted.
3. BugNomination does *not* implement IHasDateCreated as the interface
claimed, as revealed by the new unittests.
lib/
lib/
lib/
lib/
Preview Diff
1 | === removed file 'lib/lp/bugs/doc/bug-nomination.txt' |
2 | --- lib/lp/bugs/doc/bug-nomination.txt 2012-08-07 02:31:56 +0000 |
3 | +++ lib/lp/bugs/doc/bug-nomination.txt 1970-01-01 00:00:00 +0000 |
4 | @@ -1,570 +0,0 @@ |
5 | -Bug Nomination |
6 | -============== |
7 | - |
8 | -A bug supervisor can nominate a bug to be fixed in a specific |
9 | -distribution or product series. Nominations are created by |
10 | -calling IBug.addNomination. |
11 | - |
12 | - >>> from zope.component import getUtility |
13 | - >>> from zope.interface.verify import verifyClass |
14 | - >>> from zope.security.proxy import removeSecurityProxy |
15 | - >>> from lp.testing import login_person |
16 | - >>> from lp.bugs.interfaces.bug import IBugSet |
17 | - >>> from lp.bugs.interfaces.bugnomination import IBugNomination |
18 | - >>> from lp.bugs.model.bugnomination import BugNomination |
19 | - >>> from lp.registry.interfaces.distribution import IDistributionSet |
20 | - >>> from lp.registry.interfaces.person import IPersonSet |
21 | - >>> from lp.registry.interfaces.product import IProductSet |
22 | - >>> from lp.testing.sampledata import (ADMIN_EMAIL) |
23 | - >>> login(ADMIN_EMAIL) |
24 | - >>> nominator = factory.makePerson(name='nominator') |
25 | - >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu") |
26 | - >>> ubuntu = removeSecurityProxy(ubuntu) |
27 | - >>> ubuntu.bug_supervisor = nominator |
28 | - >>> firefox = getUtility(IProductSet).getByName("firefox") |
29 | - >>> firefox = removeSecurityProxy(firefox) |
30 | - >>> firefox.bug_supervisor = nominator |
31 | - >>> ignored = login_person(nominator) |
32 | - |
33 | -The BugNomination class implements IBugNomination. |
34 | - |
35 | - >>> verifyClass(IBugNomination, BugNomination) |
36 | - True |
37 | - |
38 | - >>> bugset = getUtility(IBugSet) |
39 | - >>> bug_one = bugset.get(1) |
40 | - |
41 | - >>> ubuntu_grumpy = ubuntu.getSeries("grumpy") |
42 | - >>> personset = getUtility(IPersonSet) |
43 | - >>> nominator = personset.getByName("nominator") |
44 | - |
45 | - >>> grumpy_nomination = bug_one.addNomination( |
46 | - ... target=ubuntu_grumpy, owner=nominator) |
47 | - |
48 | -The nomination records the distro series or series for which the bug |
49 | -was nominated and the user that submitted the nomination (the "owner"). |
50 | - |
51 | - >>> print grumpy_nomination.owner.name |
52 | - nominator |
53 | - |
54 | - >>> print grumpy_nomination.distroseries.fullseriesname |
55 | - Ubuntu Grumpy |
56 | - |
57 | -Let's create another nomination, this time on a product series. |
58 | - |
59 | - >>> from lp.registry.interfaces.product import IProductSet |
60 | - |
61 | - >>> firefox = getUtility(IProductSet).getByName("firefox") |
62 | - |
63 | - >>> firefox_trunk = firefox.getSeries("trunk") |
64 | - |
65 | - >>> nominator = personset.getByName("nominator") |
66 | - |
67 | - >>> firefox_ms_nomination = bug_one.addNomination( |
68 | - ... target=firefox_trunk, owner=nominator) |
69 | - |
70 | - >>> print firefox_ms_nomination.owner.name |
71 | - nominator |
72 | - |
73 | - >>> print firefox_ms_nomination.productseries.title |
74 | - Mozilla Firefox trunk series |
75 | - |
76 | -The target of a nomination can also be accessed through its target |
77 | -attribute. |
78 | - |
79 | - >>> print grumpy_nomination.target.bugtargetdisplayname |
80 | - Ubuntu Grumpy |
81 | - |
82 | - >>> print firefox_ms_nomination.target.bugtargetdisplayname |
83 | - Mozilla Firefox trunk |
84 | - |
85 | -Use IBug.canBeNominatedFor to see if a bug can be nominated for a |
86 | -particular distroseries or productseries. This will consider whether |
87 | -the bug has already been nominated for that series, or even already |
88 | -targeted to that series without a nomination, which can happen for bugs |
89 | -that were reported prior to the release management/nomination |
90 | -functionality existing. |
91 | - |
92 | - >>> ubuntu_breezy_autotest = ubuntu.getSeries("breezy-autotest") |
93 | - |
94 | - >>> bug_one.canBeNominatedFor(firefox_trunk) |
95 | - False |
96 | - |
97 | - >>> bug_one.canBeNominatedFor(ubuntu_grumpy) |
98 | - False |
99 | - |
100 | - >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest) |
101 | - True |
102 | - |
103 | -Bug five is already targeted to Ubuntu Warty, so even though it has no |
104 | -Warty nominations, it cannot be targeted to Warty. |
105 | - |
106 | - >>> bug_five = bugset.get(5) |
107 | - |
108 | - >>> def by_bugtargetdisplayname(bugtask): |
109 | - ... return bugtask.target.bugtargetdisplayname.lower() |
110 | - |
111 | - >>> tasks = sorted(bug_five.bugtasks, key=by_bugtargetdisplayname) |
112 | - |
113 | - >>> for task in tasks: |
114 | - ... print task.target.bugtargetdisplayname |
115 | - Mozilla Firefox |
116 | - Mozilla Firefox 1.0 |
117 | - mozilla-firefox (Ubuntu Warty) |
118 | - |
119 | - >>> ubuntu_warty = ubuntu.getSeries("warty") |
120 | - >>> bug_five.canBeNominatedFor(ubuntu_warty) |
121 | - False |
122 | - |
123 | -The getNominationFor() method returns a nomination for a specific |
124 | -productseries or distroseries. If there is no nomination for the target |
125 | -provided, a NotFoundError is raised. |
126 | - |
127 | - >>> bug_one.getNominationFor(firefox_trunk) |
128 | - <BugNomination ...> |
129 | - |
130 | - >>> bug_one.getNominationFor(ubuntu_grumpy) |
131 | - <BugNomination ...> |
132 | - |
133 | - >>> bug_one.getNominationFor(ubuntu_breezy_autotest) |
134 | - Traceback (most recent call last): |
135 | - ... |
136 | - NotFoundError: ... |
137 | - |
138 | -IBug.getNominations() returns a list of all IBugNominations for a bug, |
139 | -ordered by IBugTarget.bugtargetdisplayname. |
140 | - |
141 | - >>> nominations = bug_one.getNominations() |
142 | - |
143 | - >>> [nomination.target.bugtargetdisplayname for nomination in nominations] |
144 | - [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk', |
145 | - u'Ubuntu Grumpy', u'Ubuntu Hoary'] |
146 | - |
147 | -This method also accepts a target argument, for further filtering. |
148 | - |
149 | - >>> nominations = bug_one.getNominations(firefox) |
150 | - |
151 | - >>> [nomination.target.bugtargetdisplayname for nomination in nominations] |
152 | - [u'Mozilla Firefox 1.0', u'Mozilla Firefox trunk'] |
153 | - |
154 | - >>> nominations = bug_one.getNominations(ubuntu) |
155 | - |
156 | - >>> [nomination.target.bugtargetdisplayname for nomination in nominations] |
157 | - [u'Ubuntu Grumpy', u'Ubuntu Hoary'] |
158 | - |
159 | - |
160 | -Nomination Status |
161 | ------------------ |
162 | - |
163 | -A nomination is created with an initial status of "Nominated". |
164 | -Internally this state is called PROPOSED, but in the UI we display it |
165 | -as "Nominated". |
166 | - |
167 | - >>> ubuntu_breezy_autotest_nomination = bug_one.addNomination( |
168 | - ... target=ubuntu_breezy_autotest, owner=nominator) |
169 | - |
170 | - >>> print ubuntu_breezy_autotest_nomination.status.title |
171 | - Nominated |
172 | - >>> ubuntu_breezy_autotest_nomination.isProposed() |
173 | - True |
174 | - >>> ubuntu_breezy_autotest_nomination.isApproved() |
175 | - False |
176 | - >>> ubuntu_breezy_autotest_nomination.isDeclined() |
177 | - False |
178 | - |
179 | -Nomination status changes have an associated workflow. For this reason, |
180 | -setting status directly is not possible. |
181 | - |
182 | - >>> from lp.bugs.interfaces.bugnomination import BugNominationStatus |
183 | - |
184 | - >>> nomination.status = BugNominationStatus.APPROVED |
185 | - Traceback (most recent call last): |
186 | - ... |
187 | - ForbiddenAttribute: ... |
188 | - |
189 | -The status of a nomination is changed by calling either the approve() or |
190 | -decline() method. Only users with launchpad.Driver permission on the |
191 | -nomination can approve or decline it. |
192 | - |
193 | - >>> from lp.services.webapp.authorization import check_permission |
194 | - >>> from lp.services.webapp.interfaces import ILaunchBag |
195 | - |
196 | - >>> current_user = getUtility(ILaunchBag).user |
197 | - |
198 | - >>> current_user == nominator |
199 | - True |
200 | - >>> check_permission("launchpad.Driver", firefox_ms_nomination) |
201 | - False |
202 | - |
203 | - >>> firefox_ms_nomination.approve(nominator) |
204 | - Traceback (most recent call last): |
205 | - .. |
206 | - Unauthorized: ... |
207 | - |
208 | - >>> firefox_ms_nomination.decline(nominator) |
209 | - Traceback (most recent call last): |
210 | - .. |
211 | - Unauthorized: ... |
212 | - |
213 | -(Log in as an admin to set the driver.) |
214 | - |
215 | - >>> login("foo.bar@canonical.com") |
216 | - |
217 | - >>> no_privs = personset.getByName("no-priv") |
218 | - >>> firefox_ms_nomination.target.driver = no_privs |
219 | - |
220 | - >>> login("no-priv@canonical.com") |
221 | - |
222 | - |
223 | -Approving a nomination |
224 | ----------------------- |
225 | - |
226 | -When a nomination is approved, the appropriate bugtask(s) are created on |
227 | -the target of the nomination and the status is set to APPROVED. |
228 | - |
229 | -For example, there are currently no bugtasks on the firefox_trunk |
230 | -productseries. |
231 | - |
232 | - >>> from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams |
233 | - |
234 | - >>> params = BugTaskSearchParams(user=no_privs, bug=bug_one) |
235 | - >>> found_tasks = firefox_trunk.searchTasks(params) |
236 | - >>> found_tasks.count() |
237 | - 0 |
238 | - |
239 | -When a nomination is approved, one task is created, targeted at |
240 | -firefox_trunk. |
241 | - |
242 | - >>> firefox_ms_nomination.approve(no_privs) |
243 | - |
244 | - >>> firefox_ms_nomination.isApproved() |
245 | - True |
246 | - >>> firefox_ms_nomination.isProposed() |
247 | - False |
248 | - >>> firefox_ms_nomination.isDeclined() |
249 | - False |
250 | - |
251 | - >>> found_tasks.count() |
252 | - 1 |
253 | - >>> bugtask = found_tasks[0] |
254 | - >>> bugtask.target == firefox_trunk |
255 | - True |
256 | - >>> print bugtask.owner.name |
257 | - no-priv |
258 | - |
259 | -When a distribution bug nomination is approved, a task is created for |
260 | -each package the bug affects in that distro. For example, let's ensure |
261 | -bug #1 affects more than one Ubuntu package. |
262 | - |
263 | - >>> from lp.bugs.interfaces.bugtask import IBugTaskSet |
264 | - |
265 | - >>> ubuntu_tbird = ubuntu.getSourcePackage("thunderbird") |
266 | - >>> ignore = factory.makeSourcePackagePublishingHistory( |
267 | - ... distroseries=ubuntu.currentseries, |
268 | - ... sourcepackagename=ubuntu_tbird.sourcepackagename) |
269 | - |
270 | - >>> getUtility(IBugTaskSet).createTask(bug_one, no_privs, ubuntu_tbird) |
271 | - <BugTask ...> |
272 | - |
273 | - >>> tasks = sorted( |
274 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
275 | - |
276 | - >>> for task in tasks: |
277 | - ... print task.target.bugtargetdisplayname |
278 | - Mozilla Firefox |
279 | - Mozilla Firefox trunk |
280 | - mozilla-firefox (Debian) |
281 | - mozilla-firefox (Ubuntu) |
282 | - thunderbird (Ubuntu) |
283 | - |
284 | -When we approve the nomination, two more Ubuntu tasks are added for the |
285 | -Grumpy series. The user that made the decision is stored in the decider |
286 | -attribute. The date on which the decision was made is stored in the |
287 | -date_decided attribute. |
288 | - |
289 | -(Again, first we'll set the driver with an admin user, to ensure |
290 | -no_privs can actually approve the nomination.) |
291 | - |
292 | - >>> login("foo.bar@canonical.com") |
293 | - >>> grumpy_nomination.target.driver = no_privs |
294 | - >>> login("no-priv@canonical.com") |
295 | - |
296 | - >>> grumpy_nomination.date_decided is None |
297 | - True |
298 | - >>> grumpy_nomination.approve(no_privs) |
299 | - >>> print grumpy_nomination.status.title |
300 | - Approved |
301 | - >>> print grumpy_nomination.decider.name |
302 | - no-priv |
303 | - >>> grumpy_nomination.date_decided |
304 | - datetime... |
305 | - |
306 | - >>> tasks = sorted( |
307 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
308 | - |
309 | - >>> for task in tasks: |
310 | - ... print task.target.bugtargetdisplayname |
311 | - Mozilla Firefox |
312 | - Mozilla Firefox trunk |
313 | - mozilla-firefox (Debian) |
314 | - mozilla-firefox (Ubuntu Grumpy) |
315 | - mozilla-firefox (Ubuntu) |
316 | - thunderbird (Ubuntu Grumpy) |
317 | - thunderbird (Ubuntu) |
318 | - |
319 | -Let's now nominate for Warty. no_privs is the driver, so will have |
320 | -no problems. |
321 | - |
322 | - >>> ubuntu_warty = ubuntu.getSeries("warty") |
323 | - >>> login("foo.bar@canonical.com") |
324 | - >>> ubuntu_warty.driver = no_privs |
325 | - >>> login("no-priv@canonical.com") |
326 | - |
327 | - >>> warty_nomination = bug_one.addNomination( |
328 | - ... target=ubuntu_warty, owner=no_privs) |
329 | - >>> warty_nomination.approve(no_privs) |
330 | - |
331 | - >>> print warty_nomination.status.title |
332 | - Approved |
333 | - >>> print warty_nomination.decider.name |
334 | - no-priv |
335 | - >>> warty_nomination.date_decided |
336 | - datetime... |
337 | - |
338 | - >>> tasks = sorted( |
339 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
340 | - |
341 | - >>> for task in tasks: |
342 | - ... print task.target.bugtargetdisplayname |
343 | - Mozilla Firefox |
344 | - Mozilla Firefox trunk |
345 | - mozilla-firefox (Debian) |
346 | - mozilla-firefox (Ubuntu Grumpy) |
347 | - mozilla-firefox (Ubuntu Warty) |
348 | - mozilla-firefox (Ubuntu) |
349 | - thunderbird (Ubuntu Grumpy) |
350 | - thunderbird (Ubuntu Warty) |
351 | - thunderbird (Ubuntu) |
352 | - |
353 | - >>> login("foo.bar@canonical.com") |
354 | - >>> ubuntu_warty.driver = None |
355 | - |
356 | - |
357 | -Declining a nomination |
358 | ----------------------- |
359 | - |
360 | -Declining a nomination simply sets its status to DECLINED. No tasks are |
361 | -created. |
362 | - |
363 | - >>> login("foo.bar@canonical.com") |
364 | - >>> ubuntu_breezy_autotest_nomination.target.driver = no_privs |
365 | - >>> login("no-priv@canonical.com") |
366 | - |
367 | - >>> ubuntu_breezy_autotest_nomination.date_decided is None |
368 | - True |
369 | - >>> print ubuntu_breezy_autotest_nomination.status.title |
370 | - Nominated |
371 | - |
372 | - >>> ubuntu_breezy_autotest_nomination.decline(no_privs) |
373 | - |
374 | - >>> print ubuntu_breezy_autotest_nomination.status.title |
375 | - Declined |
376 | - |
377 | - >>> ubuntu_breezy_autotest_nomination.isDeclined() |
378 | - True |
379 | - >>> ubuntu_breezy_autotest_nomination.isApproved() |
380 | - False |
381 | - >>> ubuntu_breezy_autotest_nomination.isProposed() |
382 | - False |
383 | - >>> print ubuntu_breezy_autotest_nomination.decider.name |
384 | - no-priv |
385 | - >>> ubuntu_breezy_autotest_nomination.date_decided |
386 | - datetime... |
387 | - |
388 | -If a nomination is declined, the bug can be re-nominated for the same target. |
389 | -The decider and date declined are reset to None. |
390 | - |
391 | - >>> bug_one.canBeNominatedFor(ubuntu_breezy_autotest) |
392 | - True |
393 | - >>> breezy_nomination = bug_one.addNomination( |
394 | - ... target=ubuntu_breezy_autotest, owner=no_privs) |
395 | - >>> ubuntu_breezy_autotest_nomination.isApproved() |
396 | - False |
397 | - >>> breezy_nomination.isDeclined() |
398 | - False |
399 | - >>> breezy_nomination.isProposed() |
400 | - True |
401 | - >>> print breezy_nomination.decider |
402 | - None |
403 | - >>> print breezy_nomination.date_decided |
404 | - None |
405 | - |
406 | - |
407 | -Automatic targeting of new source packages |
408 | ------------------------------------------- |
409 | - |
410 | -If a another distribution task is added, and nomination for that |
411 | -distribution's series already exists, the nominations will be valid |
412 | -for the new task as well, and bugtasks will be created for all accepted |
413 | -ones. |
414 | - |
415 | -The nominations are per distroseries, they are not source package |
416 | -specific, so they are automatically valid for new bugtasks. What's |
417 | -important are the accepted nominations. Bug one has an accepted |
418 | -nomination for Grumpy and Warty: |
419 | - |
420 | - >>> accepted_nominations = [ |
421 | - ... nomination for nomination in bug_one.getNominations(ubuntu) |
422 | - ... if nomination.isApproved()] |
423 | - >>> for nomination in accepted_nominations: |
424 | - ... print nomination.distroseries.displayname |
425 | - Grumpy |
426 | - Warty |
427 | - |
428 | -So if we create a new bugtask on evolution (Ubuntu), a task for |
429 | -evolution (Ubuntu Grumpy) and evolution (Ubuntu Warty) will be created |
430 | -automatically. |
431 | - |
432 | - >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution') |
433 | - >>> getUtility(IBugTaskSet).createTask( |
434 | - ... bug_one, no_privs, ubuntu_evolution) |
435 | - <BugTask ...> |
436 | - |
437 | - >>> tasks = sorted( |
438 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
439 | - |
440 | - >>> for task in tasks: |
441 | - ... print task.target.bugtargetdisplayname |
442 | - evolution (Ubuntu Grumpy) |
443 | - evolution (Ubuntu Warty) |
444 | - evolution (Ubuntu) |
445 | - ... |
446 | - |
447 | - |
448 | -Changing the Source Package of a Targeted Bugtask |
449 | -------------------------------------------------- |
450 | - |
451 | -The nomination model requires that a generic distribution task exists |
452 | -for each distroseries task. This causes some problem when renaming the |
453 | -source package on an accepted nomination. For example, if we would |
454 | -change the thunderbird package on the Grumpy task, it won't have a |
455 | -corresponding generic distribution task. |
456 | - |
457 | -The way we tie nominations to distribution series, and not to source |
458 | -packages, makes it hard to solve source package changes in a nice way. |
459 | -So what happens when a source package is changed is that we simply |
460 | -rename all other bugtasks which points to the same distribution and |
461 | -source package name. This is not ideal, but hopefully package renames |
462 | -after the bug has been targeted to a series is rare enough for this to |
463 | -be acceptable. |
464 | - |
465 | - >>> thunderbird_grumpy = tasks[-3] |
466 | - >>> thunderbird_grumpy.bugtargetname |
467 | - u'thunderbird (Ubuntu Grumpy)' |
468 | - |
469 | - >>> thunderbird_grumpy.transitionToTarget( |
470 | - ... ubuntu.getSeries('grumpy').getSourcePackage('pmount'), |
471 | - ... getUtility(ILaunchBag).user) |
472 | - |
473 | - >>> tasks = sorted( |
474 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
475 | - |
476 | - >>> for task in tasks: |
477 | - ... print task.target.bugtargetdisplayname |
478 | - evolution (Ubuntu Grumpy) |
479 | - evolution (Ubuntu Warty) |
480 | - evolution (Ubuntu) |
481 | - Mozilla Firefox |
482 | - Mozilla Firefox trunk |
483 | - mozilla-firefox (Debian) |
484 | - mozilla-firefox (Ubuntu Grumpy) |
485 | - mozilla-firefox (Ubuntu Warty) |
486 | - mozilla-firefox (Ubuntu) |
487 | - pmount (Ubuntu Grumpy) |
488 | - pmount (Ubuntu Warty) |
489 | - pmount (Ubuntu) |
490 | - |
491 | -The same is done if the distribution task's source package is changed. |
492 | - |
493 | - >>> pmount_ubuntu = tasks[-1] |
494 | - >>> pmount_ubuntu.bugtargetname |
495 | - u'pmount (Ubuntu)' |
496 | - |
497 | - >>> ubuntu_thunderbird = ubuntu.getSourcePackage('thunderbird') |
498 | - >>> pmount_ubuntu.transitionToTarget( |
499 | - ... ubuntu_thunderbird, getUtility(ILaunchBag).user) |
500 | - |
501 | - >>> tasks = sorted( |
502 | - ... bug_one.bugtasks, key=by_bugtargetdisplayname) |
503 | - |
504 | - >>> for task in tasks: |
505 | - ... print task.target.bugtargetdisplayname |
506 | - evolution (Ubuntu Grumpy) |
507 | - evolution (Ubuntu Warty) |
508 | - evolution (Ubuntu) |
509 | - Mozilla Firefox |
510 | - Mozilla Firefox trunk |
511 | - mozilla-firefox (Debian) |
512 | - mozilla-firefox (Ubuntu Grumpy) |
513 | - mozilla-firefox (Ubuntu Warty) |
514 | - mozilla-firefox (Ubuntu) |
515 | - thunderbird (Ubuntu Grumpy) |
516 | - thunderbird (Ubuntu Warty) |
517 | - thunderbird (Ubuntu) |
518 | - |
519 | - |
520 | -Bug Nomination Set |
521 | ------------------- |
522 | - |
523 | -IBugNominationSet is used to fetch bug nominations by ID. This is useful |
524 | -mainly in traversal code. |
525 | - |
526 | - >>> from lp.bugs.interfaces.bugnomination import IBugNominationSet |
527 | - |
528 | - >>> getUtility(IBugNominationSet).get(1) |
529 | - <BugNomination at ...> |
530 | - |
531 | -If a nomination is not found, a NotFoundError is raised. |
532 | - |
533 | - >>> getUtility(IBugNominationSet).get(-1) |
534 | - Traceback (most recent call last): |
535 | - ... |
536 | - NotFoundError: ... |
537 | - |
538 | - |
539 | -Error Handling |
540 | --------------- |
541 | - |
542 | -Trying to nominate a bug for a series for which it's already nominated |
543 | -or targeted raises a NominationError. |
544 | - |
545 | - >>> bug_one.addNomination( |
546 | - ... target=ubuntu_grumpy, owner=no_privs) |
547 | - Traceback (most recent call last): |
548 | - .. |
549 | - NominationError: ... |
550 | - |
551 | - >>> bug_one.addNomination( |
552 | - ... target=firefox_trunk, owner=no_privs) |
553 | - Traceback (most recent call last): |
554 | - .. |
555 | - NominationError: ... |
556 | - |
557 | -Nominating a bug for an obsolete distroseries raises a |
558 | -NominationSeriesObsoleteError. Let's make a new obsolete distroseries |
559 | -to demonstrate. |
560 | - |
561 | - >>> from lp.registry.interfaces.series import SeriesStatus |
562 | - |
563 | - >>> login("foo.bar@canonical.com") |
564 | - >>> ubuntu_edgy = factory.makeDistroSeries( |
565 | - ... distribution=ubuntu, version='6.10', |
566 | - ... status=SeriesStatus.OBSOLETE) |
567 | - >>> login("no-priv@canonical.com") |
568 | - |
569 | - >>> bug_one.addNomination(target=ubuntu_edgy, owner=no_privs) |
570 | - Traceback (most recent call last): |
571 | - .. |
572 | - NominationSeriesObsoleteError: ... |
573 | - |
574 | - >>> logout() |
575 | |
576 | === modified file 'lib/lp/bugs/interfaces/bugnomination.py' |
577 | --- lib/lp/bugs/interfaces/bugnomination.py 2012-01-01 02:58:52 +0000 |
578 | +++ lib/lp/bugs/interfaces/bugnomination.py 2012-10-12 18:48:23 +0000 |
579 | @@ -47,7 +47,6 @@ |
580 | ) |
581 | |
582 | from lp import _ |
583 | -from lp.app.interfaces.launchpad import IHasDateCreated |
584 | from lp.app.validators.validation import can_be_nominated_for_series |
585 | from lp.bugs.interfaces.bug import IBug |
586 | from lp.bugs.interfaces.bugtarget import IBugTarget |
587 | @@ -101,7 +100,7 @@ |
588 | """) |
589 | |
590 | |
591 | -class IBugNomination(IHasBug, IHasOwner, IHasDateCreated): |
592 | +class IBugNomination(IHasBug, IHasOwner): |
593 | """A nomination for a bug to be fixed in a specific series. |
594 | |
595 | A nomination can apply to an IDistroSeries or an IProductSeries. |
596 | |
597 | === modified file 'lib/lp/bugs/model/tests/test_bug.py' |
598 | --- lib/lp/bugs/model/tests/test_bug.py 2012-10-08 01:02:13 +0000 |
599 | +++ lib/lp/bugs/model/tests/test_bug.py 2012-10-12 18:48:23 +0000 |
600 | @@ -70,6 +70,31 @@ |
601 | nomination = bug.getNominationFor(sourcepackage) |
602 | self.assertEqual(series, nomination.target) |
603 | |
604 | + def makeManyNominations(self): |
605 | + target = self.factory.makeSourcePackage() |
606 | + series = target.distroseries |
607 | + with person_logged_in(series.distribution.owner): |
608 | + nomination = self.factory.makeBugNomination(target=target) |
609 | + bug = nomination.bug |
610 | + other_series = self.factory.makeProductSeries() |
611 | + other_target = other_series.product |
612 | + self.factory.makeBugTask(bug=bug, target=other_target) |
613 | + with person_logged_in(other_target.owner): |
614 | + other_nomination = bug.addNomination( |
615 | + other_target.owner, other_series) |
616 | + return bug, [nomination, other_nomination] |
617 | + |
618 | + def test_getNominations(self): |
619 | + # The getNominations() method returns all the nominations for the bug. |
620 | + bug, nominations = self.makeManyNominations() |
621 | + self.assertContentEqual(nominations, bug.getNominations()) |
622 | + |
623 | + def test_getNominations_with_target(self): |
624 | + # The target argument filters the nominations to just one pillar. |
625 | + bug, nominations = self.makeManyNominations() |
626 | + pillar = nominations[0].target.pillar |
627 | + self.assertContentEqual([nominations[0]], bug.getNominations(pillar)) |
628 | + |
629 | def test_markAsDuplicate_None(self): |
630 | # Calling markAsDuplicate(None) on a bug that is not currently a |
631 | # duplicate works correctly, and does not raise an AttributeError. |
632 | |
633 | === modified file 'lib/lp/bugs/tests/test_bugnomination.py' |
634 | --- lib/lp/bugs/tests/test_bugnomination.py 2012-08-08 07:22:51 +0000 |
635 | +++ lib/lp/bugs/tests/test_bugnomination.py 2012-10-12 18:48:23 +0000 |
636 | @@ -5,16 +5,200 @@ |
637 | |
638 | __metaclass__ = type |
639 | |
640 | +from zope.component import getUtility |
641 | + |
642 | +from lp.app.errors import NotFoundError |
643 | +from lp.bugs.interfaces.bugnomination import ( |
644 | + BugNominationStatusError, |
645 | + BugNominationStatus, |
646 | + IBugNomination, |
647 | + IBugNominationSet, |
648 | + ) |
649 | from lp.soyuz.interfaces.publishing import PackagePublishingStatus |
650 | from lp.testing import ( |
651 | celebrity_logged_in, |
652 | login, |
653 | logout, |
654 | + person_logged_in, |
655 | TestCaseWithFactory, |
656 | ) |
657 | from lp.testing.layers import DatabaseFunctionalLayer |
658 | |
659 | |
660 | +class BugNominationTestCase(TestCaseWithFactory): |
661 | + |
662 | + layer = DatabaseFunctionalLayer |
663 | + |
664 | + def test_implementation(self): |
665 | + # BugNomination implements IBugNomination. |
666 | + target = self.factory.makeSourcePackage() |
667 | + series = target.distroseries |
668 | + bug = self.factory.makeBug(target=target.distribution_sourcepackage) |
669 | + with person_logged_in(series.distribution.owner): |
670 | + nomination = bug.addNomination(series.distribution.owner, series) |
671 | + self.assertProvides(nomination, IBugNomination) |
672 | + self.assertEqual(series.distribution.owner, nomination.owner) |
673 | + self.assertEqual(bug, nomination.bug) |
674 | + self.assertEqual(series, nomination.distroseries) |
675 | + self.assertIsNone(nomination.productseries) |
676 | + self.assertEqual(BugNominationStatus.PROPOSED, nomination.status) |
677 | + self.assertIsNone(nomination.date_decided) |
678 | + self.assertEqual('UTC', nomination.date_created.tzname()) |
679 | + |
680 | + def test_target_distroseries(self): |
681 | + # The target property returns the distroseries if it is not None. |
682 | + target = self.factory.makeSourcePackage() |
683 | + series = target.distroseries |
684 | + with person_logged_in(series.distribution.owner): |
685 | + nomination = self.factory.makeBugNomination(target=target) |
686 | + self.assertEqual(series, nomination.distroseries) |
687 | + self.assertEqual(series, nomination.target) |
688 | + |
689 | + def test_target_productseries(self): |
690 | + # The target property returns the productseries if it is not None. |
691 | + series = self.factory.makeProductSeries() |
692 | + with person_logged_in(series.product.owner): |
693 | + nomination = self.factory.makeBugNomination(target=series) |
694 | + self.assertEqual(series, nomination.productseries) |
695 | + self.assertEqual(series, nomination.target) |
696 | + |
697 | + def test_status_proposed(self): |
698 | + # isProposed is True when the status is PROPOSED. |
699 | + series = self.factory.makeProductSeries() |
700 | + with person_logged_in(series.product.owner): |
701 | + nomination = self.factory.makeBugNomination(target=series) |
702 | + self.assertEqual(BugNominationStatus.PROPOSED, nomination.status) |
703 | + self.assertIs(True, nomination.isProposed()) |
704 | + self.assertIs(False, nomination.isDeclined()) |
705 | + self.assertIs(False, nomination.isApproved()) |
706 | + |
707 | + def test_status_declined(self): |
708 | + # isDeclined is True when the status is DECLINED. |
709 | + series = self.factory.makeProductSeries() |
710 | + with person_logged_in(series.product.owner): |
711 | + nomination = self.factory.makeBugNomination(target=series) |
712 | + nomination.decline(series.product.owner) |
713 | + self.assertEqual(BugNominationStatus.DECLINED, nomination.status) |
714 | + self.assertIs(True, nomination.isDeclined()) |
715 | + self.assertIs(False, nomination.isProposed()) |
716 | + self.assertIs(False, nomination.isApproved()) |
717 | + |
718 | + def test_status_approved(self): |
719 | + # isApproved is True when the status is APPROVED. |
720 | + series = self.factory.makeProductSeries() |
721 | + with person_logged_in(series.product.owner): |
722 | + nomination = self.factory.makeBugNomination(target=series) |
723 | + nomination.approve(series.product.owner) |
724 | + self.assertEqual(BugNominationStatus.APPROVED, nomination.status) |
725 | + self.assertIs(True, nomination.isApproved()) |
726 | + self.assertIs(False, nomination.isDeclined()) |
727 | + self.assertIs(False, nomination.isProposed()) |
728 | + |
729 | + def test_decline(self): |
730 | + # The decline method updates the status and other data. |
731 | + series = self.factory.makeProductSeries() |
732 | + with person_logged_in(series.product.owner): |
733 | + nomination = self.factory.makeBugNomination(target=series) |
734 | + bug_tasks = nomination.bug.bugtasks |
735 | + nomination.decline(series.product.owner) |
736 | + self.assertEqual(BugNominationStatus.DECLINED, nomination.status) |
737 | + self.assertIsNotNone(nomination.date_decided) |
738 | + self.assertEqual(series.product.owner, nomination.decider) |
739 | + self.assertContentEqual(bug_tasks, nomination.bug.bugtasks) |
740 | + |
741 | + def test_decline_error(self): |
742 | + # A nomination cannot be declined if it is approved. |
743 | + series = self.factory.makeProductSeries() |
744 | + with person_logged_in(series.product.owner): |
745 | + nomination = self.factory.makeBugNomination(target=series) |
746 | + nomination.approve(series.product.owner) |
747 | + self.assertRaises( |
748 | + BugNominationStatusError, |
749 | + nomination.decline, series.product.owner) |
750 | + |
751 | + def test_approve_productseries(self): |
752 | + # Approving a product nomination creates a productseries bug task. |
753 | + series = self.factory.makeProductSeries() |
754 | + with person_logged_in(series.product.owner): |
755 | + nomination = self.factory.makeBugNomination(target=series) |
756 | + bug_tasks = nomination.bug.bugtasks |
757 | + nomination.approve(series.product.owner) |
758 | + self.assertEqual(BugNominationStatus.APPROVED, nomination.status) |
759 | + self.assertIsNotNone(nomination.date_decided) |
760 | + self.assertEqual(series.product.owner, nomination.decider) |
761 | + expected_targets = [bt.target for bt in bug_tasks] + [series] |
762 | + self.assertContentEqual( |
763 | + expected_targets, [bt.target for bt in nomination.bug.bugtasks]) |
764 | + |
765 | + def test_approve_distroseries_source_package(self): |
766 | + # Approving a package nomination creates a distroseries |
767 | + # source package bug task. |
768 | + target = self.factory.makeSourcePackage() |
769 | + series = target.distroseries |
770 | + with person_logged_in(series.distribution.owner): |
771 | + nomination = self.factory.makeBugNomination(target=target) |
772 | + bug_tasks = nomination.bug.bugtasks |
773 | + nomination.approve(series.distribution.owner) |
774 | + self.assertEqual(BugNominationStatus.APPROVED, nomination.status) |
775 | + self.assertIsNotNone(nomination.date_decided) |
776 | + self.assertEqual(series.distribution.owner, nomination.decider) |
777 | + expected_targets = [bt.target for bt in bug_tasks] + [target] |
778 | + self.assertContentEqual( |
779 | + expected_targets, [bt.target for bt in nomination.bug.bugtasks]) |
780 | + |
781 | + def test_approve_distroseries_source_package_many(self): |
782 | + # Approving a package nomination creates a distroseries |
783 | + # source package bug task for each affect package in the same distro. |
784 | + # See bug 110195 which argues this is wrong. |
785 | + target = self.factory.makeSourcePackage() |
786 | + series = target.distroseries |
787 | + target2 = self.factory.makeSourcePackage(distroseries=series) |
788 | + bug = self.factory.makeBug(target=target.distribution_sourcepackage) |
789 | + self.factory.makeBugTask( |
790 | + bug=bug, target=target2.distribution_sourcepackage) |
791 | + bug_tasks = bug.bugtasks |
792 | + with person_logged_in(series.distribution.owner): |
793 | + nomination = self.factory.makeBugNomination(bug=bug, target=target) |
794 | + nomination.approve(series.distribution.owner) |
795 | + expected_targets = [bt.target for bt in bug_tasks] + [target, target2] |
796 | + self.assertContentEqual( |
797 | + expected_targets, [bt.target for bt in bug.bugtasks]) |
798 | + |
799 | + def test_approve_twice(self): |
800 | + # Approving a nomination twice is a no-op. |
801 | + series = self.factory.makeProductSeries() |
802 | + with person_logged_in(series.product.owner): |
803 | + nomination = self.factory.makeBugNomination(target=series) |
804 | + nomination.approve(series.product.owner) |
805 | + self.assertEqual(BugNominationStatus.APPROVED, nomination.status) |
806 | + date_decided = nomination.date_decided |
807 | + self.assertIsNotNone(date_decided) |
808 | + self.assertEqual(series.product.owner, nomination.decider) |
809 | + with celebrity_logged_in('admin') as admin: |
810 | + nomination.approve(admin) |
811 | + self.assertEqual(date_decided, nomination.date_decided) |
812 | + self.assertEqual(series.product.owner, nomination.decider) |
813 | + |
814 | + def test_approve_distroseries_source_package_then_retarget(self): |
815 | + # Retargeting a bugtarget with and approved nomination also |
816 | + # retargets the master bug target. |
817 | + target = self.factory.makeSourcePackage() |
818 | + series = target.distroseries |
819 | + with person_logged_in(series.distribution.owner): |
820 | + nomination = self.factory.makeBugNomination(target=target) |
821 | + nomination.approve(series.distribution.owner) |
822 | + target2 = self.factory.makeSourcePackage( |
823 | + distroseries=series, publish=True) |
824 | + product_target = nomination.bug.bugtasks[0].target |
825 | + expected_targets = [ |
826 | + product_target, target2, target2.distribution_sourcepackage] |
827 | + bug_task = nomination.bug.bugtasks[-1] |
828 | + with person_logged_in(series.distribution.owner): |
829 | + bug_task.transitionToTarget(target2, series.distribution.owner) |
830 | + self.assertContentEqual( |
831 | + expected_targets, [bt.target for bt in nomination.bug.bugtasks]) |
832 | + |
833 | + |
834 | class CanBeNominatedForTestMixin: |
835 | """Test case mixin for IBug.canBeNominatedFor.""" |
836 | |
837 | @@ -218,3 +402,19 @@ |
838 | self.assertFalse(nomination.canApprove(self.factory.makePerson())) |
839 | self.assertTrue(nomination.canApprove(package_perm.person)) |
840 | self.assertTrue(nomination.canApprove(comp_perm.person)) |
841 | + |
842 | + |
843 | +class BugNominationSetTestCase(TestCaseWithFactory): |
844 | + |
845 | + layer = DatabaseFunctionalLayer |
846 | + |
847 | + def test_get(self): |
848 | + series = self.factory.makeProductSeries() |
849 | + with person_logged_in(series.product.owner): |
850 | + nomination = self.factory.makeBugNomination(target=series) |
851 | + bug_nomination_set = getUtility(IBugNominationSet) |
852 | + self.assertEqual(nomination, bug_nomination_set.get(nomination.id)) |
853 | + |
854 | + def test_get_none(self): |
855 | + bug_nomination_set = getUtility(IBugNominationSet) |
856 | + self.assertRaises(NotFoundError, bug_nomination_set.get, -1) |
857 | |
858 | === modified file 'lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py' |
859 | --- lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 2012-08-08 07:22:51 +0000 |
860 | +++ lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py 2012-10-12 18:48:23 +0000 |
861 | @@ -5,8 +5,13 @@ |
862 | |
863 | __metaclass__ = type |
864 | |
865 | -from lp.bugs.interfaces.bugnomination import NominationError |
866 | +from lp.bugs.interfaces.bugnomination import ( |
867 | + NominationError, |
868 | + NominationSeriesObsoleteError, |
869 | + ) |
870 | +from lp.registry.interfaces.series import SeriesStatus |
871 | from lp.testing import ( |
872 | + celebrity_logged_in, |
873 | login, |
874 | login_person, |
875 | logout, |
876 | @@ -47,6 +52,14 @@ |
877 | self.bug.addNomination(self.bug_supervisor, self.series) |
878 | self.assertTrue(len(self.bug.getNominations()), 1) |
879 | |
880 | + def test_bugsupervisor_addNominationFor_with_existing_nomination(self): |
881 | + # A bug cannot be nominated twice for the same series. |
882 | + login_person(self.bug_supervisor) |
883 | + self.bug.addNomination(self.bug_supervisor, self.series) |
884 | + self.assertTrue(len(self.bug.getNominations()), 1) |
885 | + self.assertRaises(NominationError, |
886 | + self.bug.addNomination, self.user, self.series) |
887 | + |
888 | def test_owner_addNominationFor_series(self): |
889 | # A bug may be nominated for a series of a product with an |
890 | # exisiting task by the product's owner. |
891 | @@ -82,3 +95,11 @@ |
892 | self.bug.addTask(self.bug_supervisor, self.distro) |
893 | self.milestone = self.factory.makeMilestone( |
894 | distribution=self.distro) |
895 | + |
896 | + def test_bugsupervisor_addNominationFor_with_obsolete_distroseries(self): |
897 | + # A bug cannot be nominated for an obsolete series. |
898 | + with celebrity_logged_in('admin'): |
899 | + self.series.status = SeriesStatus.OBSOLETE |
900 | + login_person(self.bug_supervisor) |
901 | + self.assertRaises(NominationSeriesObsoleteError, |
902 | + self.bug.addNomination, self.user, self.series) |