Merge lp:~wgrant/launchpad/export-bug-nominations into lp:launchpad
- export-bug-nominations
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeroen T. Vermeulen | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~wgrant/launchpad/export-bug-nominations | ||||
Merge into: | lp:launchpad | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp:~wgrant/launchpad/export-bug-nominations | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+10715@code.launchpad.net |
Commit message
Description of the change
William Grant (wgrant) wrote : | # |
Jeroen T. Vermeulen (jtv) wrote : | # |
Well done!
Good branch, though there are some little warts that came up on IRC and
which you fixed interactively:
* Don't return None when looking up a malformed id if a well-formed id
that doesn't belong to any bug nomination reaises NotFoundError.
* Don't over-test isApproved() in the doctest, though it was fine as a
transitional check during development. It's already covered in
bug-
* Better not export canApprove() for any person, because it may expose
private team memberships. Just make it apply to the current user:
"Can I approve this?"
* Test tearDown belongs just below setUp.
* Don't derive a test case from another, but lift the commonality up
into a mixin class.
* You inherited a few small pieces of lint; should be easy to clean up.
With that out of the way, r=me.
Jeroen
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' | |||
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-08 06:45:42 +0000 | |||
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-24 12:06:17 +0000 | |||
4 | @@ -24,6 +24,7 @@ | |||
5 | 24 | 24 | ||
6 | 25 | from lp.bugs.interfaces.bug import IBug | 25 | from lp.bugs.interfaces.bug import IBug |
7 | 26 | from lp.bugs.interfaces.bugbranch import IBugBranch | 26 | from lp.bugs.interfaces.bugbranch import IBugBranch |
8 | 27 | from lp.bugs.interfaces.bugnomination import IBugNomination | ||
9 | 27 | from lp.bugs.interfaces.bugtask import IBugTask | 28 | from lp.bugs.interfaces.bugtask import IBugTask |
10 | 28 | from lp.bugs.interfaces.bugtarget import IHasBugs | 29 | from lp.bugs.interfaces.bugtarget import IHasBugs |
11 | 29 | from lp.soyuz.interfaces.build import ( | 30 | from lp.soyuz.interfaces.build import ( |
12 | @@ -118,6 +119,12 @@ | |||
13 | 118 | IBug, 'unlinkHWSubmission', 'submission', IHWSubmission) | 119 | IBug, 'unlinkHWSubmission', 'submission', IHWSubmission) |
14 | 119 | patch_collection_return_type( | 120 | patch_collection_return_type( |
15 | 120 | IBug, 'getHWSubmissions', IHWSubmission) | 121 | IBug, 'getHWSubmissions', IHWSubmission) |
16 | 122 | IBug['getNominations'].queryTaggedValue( | ||
17 | 123 | LAZR_WEBSERVICE_EXPORTED)['params']['nominations'].value_type.schema = ( | ||
18 | 124 | IBugNomination) | ||
19 | 125 | patch_entry_return_type(IBug, 'addNomination', IBugNomination) | ||
20 | 126 | patch_entry_return_type(IBug, 'getNominationFor', IBugNomination) | ||
21 | 127 | patch_collection_return_type(IBug, 'getNominations', IBugNomination) | ||
22 | 121 | 128 | ||
23 | 122 | patch_choice_parameter_type( | 129 | patch_choice_parameter_type( |
24 | 123 | IHasBugs, 'searchTasks', 'hardware_bus', HWBus) | 130 | IHasBugs, 'searchTasks', 'hardware_bus', HWBus) |
25 | 124 | 131 | ||
26 | === modified file 'lib/lp/bugs/browser/bug.py' | |||
27 | --- lib/lp/bugs/browser/bug.py 2009-08-21 15:33:18 +0000 | |||
28 | +++ lib/lp/bugs/browser/bug.py 2009-08-23 04:53:33 +0000 | |||
29 | @@ -58,6 +58,7 @@ | |||
30 | 58 | from lp.bugs.interfaces.bugwatch import IBugWatchSet | 58 | from lp.bugs.interfaces.bugwatch import IBugWatchSet |
31 | 59 | from lp.bugs.interfaces.cve import ICveSet | 59 | from lp.bugs.interfaces.cve import ICveSet |
32 | 60 | from lp.bugs.interfaces.bugattachment import IBugAttachmentSet | 60 | from lp.bugs.interfaces.bugattachment import IBugAttachmentSet |
33 | 61 | from lp.bugs.interfaces.bugnomination import IBugNominationSet | ||
34 | 61 | 62 | ||
35 | 62 | from canonical.launchpad.mailnotification import ( | 63 | from canonical.launchpad.mailnotification import ( |
36 | 63 | MailWrapper, format_rfc2822_date) | 64 | MailWrapper, format_rfc2822_date) |
37 | @@ -126,6 +127,13 @@ | |||
38 | 126 | if attachment is not None and attachment.bug == self.context: | 127 | if attachment is not None and attachment.bug == self.context: |
39 | 127 | return attachment | 128 | return attachment |
40 | 128 | 129 | ||
41 | 130 | @stepthrough('nominations') | ||
42 | 131 | def traverse_nominations(self, nomination_id): | ||
43 | 132 | """Traverse to a nomination by id.""" | ||
44 | 133 | if not nomination_id.isdigit(): | ||
45 | 134 | return None | ||
46 | 135 | return getUtility(IBugNominationSet).get(nomination_id) | ||
47 | 136 | |||
48 | 129 | 137 | ||
49 | 130 | class BugFacets(StandardLaunchpadFacets): | 138 | class BugFacets(StandardLaunchpadFacets): |
50 | 131 | """The links that will appear in the facet menu for an `IBug`. | 139 | """The links that will appear in the facet menu for an `IBug`. |
51 | 132 | 140 | ||
52 | === modified file 'lib/lp/bugs/browser/bugnomination.py' | |||
53 | --- lib/lp/bugs/browser/bugnomination.py 2009-06-25 00:40:31 +0000 | |||
54 | +++ lib/lp/bugs/browser/bugnomination.py 2009-08-24 10:45:09 +0000 | |||
55 | @@ -105,8 +105,9 @@ | |||
56 | 105 | target=series, owner=self.user) | 105 | target=series, owner=self.user) |
57 | 106 | 106 | ||
58 | 107 | # If the user has the permission to approve the nomination, | 107 | # If the user has the permission to approve the nomination, |
61 | 108 | # then nomination was approved automatically. | 108 | # we approve it automatically. |
62 | 109 | if nomination.isApproved(): | 109 | if nomination.canApprove(self.user): |
63 | 110 | nomination.approve(self.user) | ||
64 | 110 | approved_nominations.append( | 111 | approved_nominations.append( |
65 | 111 | nomination.target.bugtargetdisplayname) | 112 | nomination.target.bugtargetdisplayname) |
66 | 112 | else: | 113 | else: |
67 | 113 | 114 | ||
68 | === modified file 'lib/lp/bugs/browser/tests/bugtask-edit-views.txt' | |||
69 | --- lib/lp/bugs/browser/tests/bugtask-edit-views.txt 2009-08-13 19:03:36 +0000 | |||
70 | +++ lib/lp/bugs/browser/tests/bugtask-edit-views.txt 2009-08-24 10:45:09 +0000 | |||
71 | @@ -126,8 +126,7 @@ | |||
72 | 126 | >>> ubuntu_grumpy = ubuntu.getSeries('grumpy') | 126 | >>> ubuntu_grumpy = ubuntu.getSeries('grumpy') |
73 | 127 | >>> nomination = bug_two.addNomination( | 127 | >>> nomination = bug_two.addNomination( |
74 | 128 | ... target=ubuntu_grumpy, owner=getUtility(ILaunchBag).user) | 128 | ... target=ubuntu_grumpy, owner=getUtility(ILaunchBag).user) |
77 | 129 | >>> nomination.isApproved() | 129 | >>> nomination.approve(getUtility(ILaunchBag).user) |
76 | 130 | True | ||
78 | 131 | >>> transaction.commit() | 130 | >>> transaction.commit() |
79 | 132 | 131 | ||
80 | 133 | >>> ubuntu_grumpy_task = bug_two.bugtasks[5] | 132 | >>> ubuntu_grumpy_task = bug_two.bugtasks[5] |
81 | 134 | 133 | ||
82 | === modified file 'lib/lp/bugs/doc/bug-nomination.txt' | |||
83 | --- lib/lp/bugs/doc/bug-nomination.txt 2009-06-13 19:58:26 +0000 | |||
84 | +++ lib/lp/bugs/doc/bug-nomination.txt 2009-08-26 02:33:29 +0000 | |||
85 | @@ -296,8 +296,8 @@ | |||
86 | 296 | thunderbird (Ubuntu Grumpy) | 296 | thunderbird (Ubuntu Grumpy) |
87 | 297 | thunderbird (Ubuntu) | 297 | thunderbird (Ubuntu) |
88 | 298 | 298 | ||
91 | 299 | If the one nominating a goal is a driver of the series, the | 299 | Let's now nominate for Warty. no_privs is the driver, so will have |
92 | 300 | nomination will be automatically approved. | 300 | no problems. |
93 | 301 | 301 | ||
94 | 302 | >>> ubuntu_warty = ubuntu.getSeries("warty") | 302 | >>> ubuntu_warty = ubuntu.getSeries("warty") |
95 | 303 | >>> login("foo.bar@canonical.com") | 303 | >>> login("foo.bar@canonical.com") |
96 | @@ -306,6 +306,7 @@ | |||
97 | 306 | 306 | ||
98 | 307 | >>> warty_nomination = bug_one.addNomination( | 307 | >>> warty_nomination = bug_one.addNomination( |
99 | 308 | ... target=ubuntu_warty, owner=no_privs) | 308 | ... target=ubuntu_warty, owner=no_privs) |
100 | 309 | >>> warty_nomination.approve(no_privs) | ||
101 | 309 | 310 | ||
102 | 310 | >>> print warty_nomination.status.title | 311 | >>> print warty_nomination.status.title |
103 | 311 | Approved | 312 | Approved |
104 | @@ -636,18 +637,18 @@ | |||
105 | 636 | NominationError: ... | 637 | NominationError: ... |
106 | 637 | 638 | ||
107 | 638 | Nominating a bug for an obsolete distroseries raises a | 639 | Nominating a bug for an obsolete distroseries raises a |
110 | 639 | NominationSeriesObsoleteError. Let's mark warty obsolete to | 640 | NominationSeriesObsoleteError. Let's make a new obsolete distroseries |
111 | 640 | demonstrate. | 641 | to demonstrate. |
112 | 641 | 642 | ||
113 | 642 | >>> from canonical.launchpad.interfaces import DistroSeriesStatus | 643 | >>> from canonical.launchpad.interfaces import DistroSeriesStatus |
114 | 643 | 644 | ||
115 | 644 | (Temporarily log in as an admin user to change the series status.) | ||
116 | 645 | |||
117 | 646 | >>> login("foo.bar@canonical.com") | 645 | >>> login("foo.bar@canonical.com") |
119 | 647 | >>> ubuntu_warty.status = DistroSeriesStatus.OBSOLETE | 646 | >>> ubuntu_edgy = factory.makeDistroRelease( |
120 | 647 | ... distribution=ubuntu, version='6.10', | ||
121 | 648 | ... status=DistroSeriesStatus.OBSOLETE) | ||
122 | 648 | >>> login("no-priv@canonical.com") | 649 | >>> login("no-priv@canonical.com") |
123 | 649 | 650 | ||
125 | 650 | >>> bug_one.addNomination(target=ubuntu_warty, owner=no_privs) | 651 | >>> bug_one.addNomination(target=ubuntu_edgy, owner=no_privs) |
126 | 651 | Traceback (most recent call last): | 652 | Traceback (most recent call last): |
127 | 652 | .. | 653 | .. |
128 | 653 | NominationSeriesObsoleteError: ... | 654 | NominationSeriesObsoleteError: ... |
129 | 654 | 655 | ||
130 | === modified file 'lib/lp/bugs/doc/bug-set-status.txt' | |||
131 | --- lib/lp/bugs/doc/bug-set-status.txt 2009-06-12 16:36:02 +0000 | |||
132 | +++ lib/lp/bugs/doc/bug-set-status.txt 2009-08-24 10:45:09 +0000 | |||
133 | @@ -225,6 +225,9 @@ | |||
134 | 225 | >>> nomination = bug.addNomination( | 225 | >>> nomination = bug.addNomination( |
135 | 226 | ... getUtility(ILaunchBag).user, ubuntu_hoary) | 226 | ... getUtility(ILaunchBag).user, ubuntu_hoary) |
136 | 227 | >>> nomination.isApproved() | 227 | >>> nomination.isApproved() |
137 | 228 | False | ||
138 | 229 | >>> nomination.approve(getUtility(ILaunchBag).user) | ||
139 | 230 | >>> nomination.isApproved() | ||
140 | 228 | True | 231 | True |
141 | 229 | >>> login('no-priv@canonical.com') | 232 | >>> login('no-priv@canonical.com') |
142 | 230 | 233 | ||
143 | @@ -247,6 +250,9 @@ | |||
144 | 247 | >>> nomination = bug.addNomination( | 250 | >>> nomination = bug.addNomination( |
145 | 248 | ... getUtility(ILaunchBag).user, ubuntu_warty) | 251 | ... getUtility(ILaunchBag).user, ubuntu_warty) |
146 | 249 | >>> nomination.isApproved() | 252 | >>> nomination.isApproved() |
147 | 253 | False | ||
148 | 254 | >>> nomination.approve(getUtility(ILaunchBag).user) | ||
149 | 255 | >>> nomination.isApproved() | ||
150 | 250 | True | 256 | True |
151 | 251 | >>> login('no-priv@canonical.com') | 257 | >>> login('no-priv@canonical.com') |
152 | 252 | 258 | ||
153 | 253 | 259 | ||
154 | === modified file 'lib/lp/bugs/doc/bugtask.txt' | |||
155 | --- lib/lp/bugs/doc/bugtask.txt 2009-08-13 19:03:36 +0000 | |||
156 | +++ lib/lp/bugs/doc/bugtask.txt 2009-08-24 10:45:09 +0000 | |||
157 | @@ -1123,8 +1123,7 @@ | |||
158 | 1123 | ... sourcepackagenameset.queryByName('mozilla-firefox'))) | 1123 | ... sourcepackagenameset.queryByName('mozilla-firefox'))) |
159 | 1124 | <BugTask at ...> | 1124 | <BugTask at ...> |
160 | 1125 | 1125 | ||
163 | 1126 | >>> new_bug.addNomination(mark, ubuntu.currentseries) | 1126 | >>> new_bug.addNomination(mark, ubuntu.currentseries).approve(mark) |
162 | 1127 | <BugNomination at ...> | ||
164 | 1128 | 1127 | ||
165 | 1129 | The first task has been created and successfully nominated to Hoary. | 1128 | The first task has been created and successfully nominated to Hoary. |
166 | 1130 | 1129 | ||
167 | 1131 | 1130 | ||
168 | === modified file 'lib/lp/bugs/interfaces/bug.py' | |||
169 | --- lib/lp/bugs/interfaces/bug.py 2009-08-06 14:36:45 +0000 | |||
170 | +++ lib/lp/bugs/interfaces/bug.py 2009-08-24 12:06:17 +0000 | |||
171 | @@ -559,20 +559,24 @@ | |||
172 | 559 | distroseries=None): | 559 | distroseries=None): |
173 | 560 | """Create an INullBugTask and return it for the given parameters.""" | 560 | """Create an INullBugTask and return it for the given parameters.""" |
174 | 561 | 561 | ||
175 | 562 | @operation_parameters( | ||
176 | 563 | target=Reference(schema=IBugTarget, title=_('Target'))) | ||
177 | 564 | @call_with(owner=REQUEST_USER) | ||
178 | 565 | @export_factory_operation(Interface, []) | ||
179 | 562 | def addNomination(owner, target): | 566 | def addNomination(owner, target): |
180 | 563 | """Nominate a bug for an IDistroSeries or IProductSeries. | 567 | """Nominate a bug for an IDistroSeries or IProductSeries. |
181 | 564 | 568 | ||
182 | 565 | :owner: An IPerson. | 569 | :owner: An IPerson. |
183 | 566 | :target: An IDistroSeries or IProductSeries. | 570 | :target: An IDistroSeries or IProductSeries. |
184 | 567 | 571 | ||
185 | 568 | The nomination will be automatically approved, if the user has | ||
186 | 569 | permission to approve it. | ||
187 | 570 | |||
188 | 571 | This method creates and returns a BugNomination. (See | 572 | This method creates and returns a BugNomination. (See |
189 | 572 | lp.bugs.model.bugnomination.BugNomination.) | 573 | lp.bugs.model.bugnomination.BugNomination.) |
190 | 573 | """ | 574 | """ |
191 | 574 | 575 | ||
193 | 575 | def canBeNominatedFor(nomination_target): | 576 | @operation_parameters( |
194 | 577 | target=Reference(schema=IBugTarget, title=_('Target'))) | ||
195 | 578 | @export_read_operation() | ||
196 | 579 | def canBeNominatedFor(target): | ||
197 | 576 | """Can this bug nominated for this target? | 580 | """Can this bug nominated for this target? |
198 | 577 | 581 | ||
199 | 578 | :nomination_target: An IDistroSeries or IProductSeries. | 582 | :nomination_target: An IDistroSeries or IProductSeries. |
200 | @@ -580,7 +584,11 @@ | |||
201 | 580 | Returns True or False. | 584 | Returns True or False. |
202 | 581 | """ | 585 | """ |
203 | 582 | 586 | ||
205 | 583 | def getNominationFor(nomination_target): | 587 | @operation_parameters( |
206 | 588 | target=Reference(schema=IBugTarget, title=_('Target'))) | ||
207 | 589 | @operation_returns_entry(Interface) | ||
208 | 590 | @export_read_operation() | ||
209 | 591 | def getNominationFor(target): | ||
210 | 584 | """Return the IBugNomination for the target. | 592 | """Return the IBugNomination for the target. |
211 | 585 | 593 | ||
212 | 586 | If no nomination is found, a NotFoundError is raised. | 594 | If no nomination is found, a NotFoundError is raised. |
213 | @@ -588,6 +596,15 @@ | |||
214 | 588 | :param nomination_target: An IDistroSeries or IProductSeries. | 596 | :param nomination_target: An IDistroSeries or IProductSeries. |
215 | 589 | """ | 597 | """ |
216 | 590 | 598 | ||
217 | 599 | @operation_parameters( | ||
218 | 600 | target=Reference( | ||
219 | 601 | schema=IBugTarget, title=_('Target'), required=False), | ||
220 | 602 | nominations=List( | ||
221 | 603 | title=_("Nominations to search through."), | ||
222 | 604 | value_type=Reference(schema=Interface), # IBugNomination | ||
223 | 605 | required=False)) | ||
224 | 606 | @operation_returns_collection_of(Interface) # IBugNomination | ||
225 | 607 | @export_read_operation() | ||
226 | 591 | def getNominations(target=None, nominations=None): | 608 | def getNominations(target=None, nominations=None): |
227 | 592 | """Return a list of all IBugNominations for this bug. | 609 | """Return a list of all IBugNominations for this bug. |
228 | 593 | 610 | ||
229 | 594 | 611 | ||
230 | === modified file 'lib/lp/bugs/interfaces/bugnomination.py' | |||
231 | --- lib/lp/bugs/interfaces/bugnomination.py 2009-07-17 00:26:05 +0000 | |||
232 | +++ lib/lp/bugs/interfaces/bugnomination.py 2009-08-26 03:56:55 +0000 | |||
233 | @@ -19,24 +19,38 @@ | |||
234 | 19 | from zope.schema import Int, Datetime, Choice, Set | 19 | from zope.schema import Int, Datetime, Choice, Set |
235 | 20 | from zope.interface import Interface, Attribute | 20 | from zope.interface import Interface, Attribute |
236 | 21 | from lazr.enum import DBEnumeratedType, DBItem | 21 | from lazr.enum import DBEnumeratedType, DBItem |
237 | 22 | from lazr.restful.declarations import ( | ||
238 | 23 | REQUEST_USER, call_with, export_as_webservice_entry, | ||
239 | 24 | export_read_operation, export_write_operation, exported, | ||
240 | 25 | operation_parameters, webservice_error) | ||
241 | 26 | from lazr.restful.fields import Reference, ReferenceChoice | ||
242 | 22 | 27 | ||
243 | 23 | from canonical.launchpad import _ | 28 | from canonical.launchpad import _ |
244 | 24 | from canonical.launchpad.fields import PublicPersonChoice | 29 | from canonical.launchpad.fields import PublicPersonChoice |
245 | 25 | from canonical.launchpad.interfaces.launchpad import IHasBug, IHasDateCreated | 30 | from canonical.launchpad.interfaces.launchpad import IHasBug, IHasDateCreated |
246 | 31 | from lp.bugs.interfaces.bug import IBug | ||
247 | 32 | from lp.bugs.interfaces.bugtarget import IBugTarget | ||
248 | 33 | from lp.registry.interfaces.distroseries import IDistroSeries | ||
249 | 34 | from lp.registry.interfaces.productseries import IProductSeries | ||
250 | 35 | from lp.registry.interfaces.person import IPerson | ||
251 | 26 | from lp.registry.interfaces.role import IHasOwner | 36 | from lp.registry.interfaces.role import IHasOwner |
252 | 27 | from canonical.launchpad.interfaces.validation import ( | 37 | from canonical.launchpad.interfaces.validation import ( |
253 | 28 | can_be_nominated_for_serieses) | 38 | can_be_nominated_for_serieses) |
254 | 29 | 39 | ||
255 | 40 | |||
256 | 30 | class NominationError(Exception): | 41 | class NominationError(Exception): |
257 | 31 | """The bug cannot be nominated for this release.""" | 42 | """The bug cannot be nominated for this release.""" |
258 | 43 | webservice_error(400) | ||
259 | 32 | 44 | ||
260 | 33 | 45 | ||
261 | 34 | class NominationSeriesObsoleteError(Exception): | 46 | class NominationSeriesObsoleteError(Exception): |
262 | 35 | """A bug cannot be nominated for an obsolete series.""" | 47 | """A bug cannot be nominated for an obsolete series.""" |
263 | 48 | webservice_error(400) | ||
264 | 36 | 49 | ||
265 | 37 | 50 | ||
266 | 38 | class BugNominationStatusError(Exception): | 51 | class BugNominationStatusError(Exception): |
267 | 39 | """A error occurred while trying to set a bug nomination status.""" | 52 | """A error occurred while trying to set a bug nomination status.""" |
268 | 53 | webservice_error(400) | ||
269 | 40 | 54 | ||
270 | 41 | 55 | ||
271 | 42 | class BugNominationStatus(DBEnumeratedType): | 56 | class BugNominationStatus(DBEnumeratedType): |
272 | @@ -72,38 +86,43 @@ | |||
273 | 72 | 86 | ||
274 | 73 | A nomination can apply to an IDistroSeries or an IProductSeries. | 87 | A nomination can apply to an IDistroSeries or an IProductSeries. |
275 | 74 | """ | 88 | """ |
276 | 89 | export_as_webservice_entry() | ||
277 | 90 | |||
278 | 75 | # We want to customize the titles and descriptions of some of the | 91 | # We want to customize the titles and descriptions of some of the |
279 | 76 | # attributes of our parent interfaces, so we redefine those specific | 92 | # attributes of our parent interfaces, so we redefine those specific |
280 | 77 | # attributes below. | 93 | # attributes below. |
281 | 78 | id = Int(title=_("Bug Nomination #")) | 94 | id = Int(title=_("Bug Nomination #")) |
284 | 79 | bug = Int(title=_("Bug #")) | 95 | bug = exported(Reference(schema=IBug, readonly=True)) |
285 | 80 | date_created = Datetime( | 96 | date_created = exported(Datetime( |
286 | 81 | title=_("Date Submitted"), | 97 | title=_("Date Submitted"), |
287 | 82 | description=_("The date on which this nomination was submitted."), | 98 | description=_("The date on which this nomination was submitted."), |
290 | 83 | required=True, readonly=True) | 99 | required=True, readonly=True)) |
291 | 84 | date_decided = Datetime( | 100 | date_decided = exported(Datetime( |
292 | 85 | title=_("Date Decided"), | 101 | title=_("Date Decided"), |
293 | 86 | description=_( | 102 | description=_( |
294 | 87 | "The date on which this nomination was approved or declined."), | 103 | "The date on which this nomination was approved or declined."), |
303 | 88 | required=False, readonly=True) | 104 | required=False, readonly=True)) |
304 | 89 | distroseries = Choice( | 105 | distroseries = exported(ReferenceChoice( |
305 | 90 | title=_("Series"), required=False, | 106 | title=_("Series"), required=False, readonly=True, |
306 | 91 | vocabulary="DistroSeries") | 107 | vocabulary="DistroSeries", schema=IDistroSeries)) |
307 | 92 | productseries = Choice( | 108 | productseries = exported(ReferenceChoice( |
308 | 93 | title=_("Series"), required=False, | 109 | title=_("Series"), required=False, readonly=True, |
309 | 94 | vocabulary="ProductSeries") | 110 | vocabulary="ProductSeries", schema=IProductSeries)) |
310 | 95 | owner = PublicPersonChoice( | 111 | owner = exported(PublicPersonChoice( |
311 | 96 | title=_('Submitter'), required=True, readonly=True, | 112 | title=_('Submitter'), required=True, readonly=True, |
314 | 97 | vocabulary='ValidPersonOrTeam') | 113 | vocabulary='ValidPersonOrTeam')) |
315 | 98 | decider = PublicPersonChoice( | 114 | decider = exported(PublicPersonChoice( |
316 | 99 | title=_('Decided By'), required=False, readonly=True, | 115 | title=_('Decided By'), required=False, readonly=True, |
321 | 100 | vocabulary='ValidPersonOrTeam') | 116 | vocabulary='ValidPersonOrTeam')) |
322 | 101 | target = Attribute( | 117 | target = exported(Reference( |
323 | 102 | "The IProductSeries or IDistroSeries of this nomination.") | 118 | schema=IBugTarget, |
324 | 103 | status = Choice( | 119 | title=_("The IProductSeries or IDistroSeries of this nomination."))) |
325 | 120 | status = exported(Choice( | ||
326 | 104 | title=_("Status"), vocabulary=BugNominationStatus, | 121 | title=_("Status"), vocabulary=BugNominationStatus, |
328 | 105 | default=BugNominationStatus.PROPOSED) | 122 | default=BugNominationStatus.PROPOSED, readonly=True)) |
329 | 106 | 123 | ||
330 | 124 | @call_with(approver=REQUEST_USER) | ||
331 | 125 | @export_write_operation() | ||
332 | 107 | def approve(approver): | 126 | def approve(approver): |
333 | 108 | """Approve this bug for fixing in a series. | 127 | """Approve this bug for fixing in a series. |
334 | 109 | 128 | ||
335 | @@ -117,6 +136,8 @@ | |||
336 | 117 | /already/ approved, this method is a noop. | 136 | /already/ approved, this method is a noop. |
337 | 118 | """ | 137 | """ |
338 | 119 | 138 | ||
339 | 139 | @call_with(decliner=REQUEST_USER) | ||
340 | 140 | @export_write_operation() | ||
341 | 120 | def decline(decliner): | 141 | def decline(decliner): |
342 | 121 | """Decline this bug for fixing in a series. | 142 | """Decline this bug for fixing in a series. |
343 | 122 | 143 | ||
344 | @@ -139,6 +160,8 @@ | |||
345 | 139 | def isApproved(): | 160 | def isApproved(): |
346 | 140 | """Is this nomination in Approved state?""" | 161 | """Is this nomination in Approved state?""" |
347 | 141 | 162 | ||
348 | 163 | @operation_parameters(person=Reference(schema=IPerson)) | ||
349 | 164 | @export_read_operation() | ||
350 | 142 | def canApprove(person): | 165 | def canApprove(person): |
351 | 143 | """Is this person allowed to approve the nomination?""" | 166 | """Is this person allowed to approve the nomination?""" |
352 | 144 | 167 | ||
353 | 145 | 168 | ||
354 | === modified file 'lib/lp/bugs/model/bug.py' | |||
355 | --- lib/lp/bugs/model/bug.py 2009-08-04 12:36:46 +0000 | |||
356 | +++ lib/lp/bugs/model/bug.py 2009-08-26 01:54:39 +0000 | |||
357 | @@ -1073,74 +1073,79 @@ | |||
358 | 1073 | 1073 | ||
359 | 1074 | def addNomination(self, owner, target): | 1074 | def addNomination(self, owner, target): |
360 | 1075 | """See `IBug`.""" | 1075 | """See `IBug`.""" |
361 | 1076 | if not self.canBeNominatedFor(target): | ||
362 | 1077 | raise NominationError( | ||
363 | 1078 | "This bug cannot be nominated for %s." % | ||
364 | 1079 | target.bugtargetdisplayname) | ||
365 | 1080 | |||
366 | 1076 | distroseries = None | 1081 | distroseries = None |
367 | 1077 | productseries = None | 1082 | productseries = None |
368 | 1078 | if IDistroSeries.providedBy(target): | 1083 | if IDistroSeries.providedBy(target): |
369 | 1079 | distroseries = target | 1084 | distroseries = target |
370 | 1080 | target_displayname = target.fullseriesname | ||
371 | 1081 | if target.status == DistroSeriesStatus.OBSOLETE: | 1085 | if target.status == DistroSeriesStatus.OBSOLETE: |
372 | 1082 | raise NominationSeriesObsoleteError( | 1086 | raise NominationSeriesObsoleteError( |
374 | 1083 | "%s is an obsolete series." % target_displayname) | 1087 | "%s is an obsolete series." % target.bugtargetdisplayname) |
375 | 1084 | else: | 1088 | else: |
376 | 1085 | assert IProductSeries.providedBy(target) | 1089 | assert IProductSeries.providedBy(target) |
377 | 1086 | productseries = target | 1090 | productseries = target |
378 | 1087 | target_displayname = target.title | ||
379 | 1088 | |||
380 | 1089 | if not self.canBeNominatedFor(target): | ||
381 | 1090 | raise NominationError( | ||
382 | 1091 | "This bug cannot be nominated for %s." % target_displayname) | ||
383 | 1092 | 1091 | ||
384 | 1093 | nomination = BugNomination( | 1092 | nomination = BugNomination( |
385 | 1094 | owner=owner, bug=self, distroseries=distroseries, | 1093 | owner=owner, bug=self, distroseries=distroseries, |
386 | 1095 | productseries=productseries) | 1094 | productseries=productseries) |
391 | 1096 | if nomination.canApprove(owner): | 1095 | self.addChange(SeriesNominated(UTC_NOW, owner, target)) |
388 | 1097 | nomination.approve(owner) | ||
389 | 1098 | else: | ||
390 | 1099 | self.addChange(SeriesNominated(UTC_NOW, owner, target)) | ||
392 | 1100 | return nomination | 1096 | return nomination |
393 | 1101 | 1097 | ||
395 | 1102 | def canBeNominatedFor(self, nomination_target): | 1098 | def canBeNominatedFor(self, target): |
396 | 1103 | """See `IBug`.""" | 1099 | """See `IBug`.""" |
397 | 1104 | try: | 1100 | try: |
399 | 1105 | self.getNominationFor(nomination_target) | 1101 | self.getNominationFor(target) |
400 | 1106 | except NotFoundError: | 1102 | except NotFoundError: |
401 | 1107 | # No nomination exists. Let's see if the bug is already | 1103 | # No nomination exists. Let's see if the bug is already |
407 | 1108 | # directly targeted to this nomination_target. | 1104 | # directly targeted to this nomination target. |
408 | 1109 | if IDistroSeries.providedBy(nomination_target): | 1105 | if IDistroSeries.providedBy(target): |
409 | 1110 | target_getter = operator.attrgetter("distroseries") | 1106 | series_getter = operator.attrgetter("distroseries") |
410 | 1111 | elif IProductSeries.providedBy(nomination_target): | 1107 | pillar_getter = operator.attrgetter("distribution") |
411 | 1112 | target_getter = operator.attrgetter("productseries") | 1108 | elif IProductSeries.providedBy(target): |
412 | 1109 | series_getter = operator.attrgetter("productseries") | ||
413 | 1110 | pillar_getter = operator.attrgetter("product") | ||
414 | 1113 | else: | 1111 | else: |
418 | 1114 | raise AssertionError( | 1112 | return False |
416 | 1115 | "Expected IDistroSeries or IProductSeries target. " | ||
417 | 1116 | "Got %r." % nomination_target) | ||
419 | 1117 | 1113 | ||
420 | 1118 | for task in self.bugtasks: | 1114 | for task in self.bugtasks: |
422 | 1119 | if target_getter(task) == nomination_target: | 1115 | if series_getter(task) == target: |
423 | 1120 | # The bug is already targeted at this | 1116 | # The bug is already targeted at this |
425 | 1121 | # nomination_target. | 1117 | # nomination target. |
426 | 1122 | return False | 1118 | return False |
427 | 1123 | 1119 | ||
428 | 1124 | # No nomination or tasks are targeted at this | 1120 | # No nomination or tasks are targeted at this |
431 | 1125 | # nomination_target. | 1121 | # nomination target. But we also don't want to nominate for a |
432 | 1126 | return True | 1122 | # series of a product or distro for which we don't have a |
433 | 1123 | # plain pillar task. | ||
434 | 1124 | for task in self.bugtasks: | ||
435 | 1125 | if pillar_getter(task) == pillar_getter(target): | ||
436 | 1126 | return True | ||
437 | 1127 | |||
438 | 1128 | # No tasks match the candidate's pillar. We must refuse. | ||
439 | 1129 | return False | ||
440 | 1127 | else: | 1130 | else: |
442 | 1128 | # The bug is already nominated for this nomination_target. | 1131 | # The bug is already nominated for this nomination target. |
443 | 1129 | return False | 1132 | return False |
444 | 1130 | 1133 | ||
446 | 1131 | def getNominationFor(self, nomination_target): | 1134 | def getNominationFor(self, target): |
447 | 1132 | """See `IBug`.""" | 1135 | """See `IBug`.""" |
450 | 1133 | if IDistroSeries.providedBy(nomination_target): | 1136 | if IDistroSeries.providedBy(target): |
451 | 1134 | filter_args = dict(distroseriesID=nomination_target.id) | 1137 | filter_args = dict(distroseriesID=target.id) |
452 | 1138 | elif IProductSeries.providedBy(target): | ||
453 | 1139 | filter_args = dict(productseriesID=target.id) | ||
454 | 1135 | else: | 1140 | else: |
456 | 1136 | filter_args = dict(productseriesID=nomination_target.id) | 1141 | return None |
457 | 1137 | 1142 | ||
458 | 1138 | nomination = BugNomination.selectOneBy(bugID=self.id, **filter_args) | 1143 | nomination = BugNomination.selectOneBy(bugID=self.id, **filter_args) |
459 | 1139 | 1144 | ||
460 | 1140 | if nomination is None: | 1145 | if nomination is None: |
461 | 1141 | raise NotFoundError( | 1146 | raise NotFoundError( |
462 | 1142 | "Bug #%d is not nominated for %s." % ( | 1147 | "Bug #%d is not nominated for %s." % ( |
464 | 1143 | self.id, nomination_target.displayname)) | 1148 | self.id, target.displayname)) |
465 | 1144 | 1149 | ||
466 | 1145 | return nomination | 1150 | return nomination |
467 | 1146 | 1151 | ||
468 | 1147 | 1152 | ||
469 | === modified file 'lib/lp/bugs/model/bugnomination.py' | |||
470 | --- lib/lp/bugs/model/bugnomination.py 2009-06-25 00:40:31 +0000 | |||
471 | +++ lib/lp/bugs/model/bugnomination.py 2009-08-23 07:15:30 +0000 | |||
472 | @@ -33,7 +33,8 @@ | |||
473 | 33 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities | 33 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
474 | 34 | from canonical.launchpad.webapp.interfaces import NotFoundError | 34 | from canonical.launchpad.webapp.interfaces import NotFoundError |
475 | 35 | from lp.bugs.interfaces.bugnomination import ( | 35 | from lp.bugs.interfaces.bugnomination import ( |
477 | 36 | BugNominationStatus, IBugNomination, IBugNominationSet) | 36 | BugNominationStatus, BugNominationStatusError, IBugNomination, |
478 | 37 | IBugNominationSet) | ||
479 | 37 | from lp.registry.interfaces.person import validate_public_person | 38 | from lp.registry.interfaces.person import validate_public_person |
480 | 38 | 39 | ||
481 | 39 | class BugNomination(SQLBase): | 40 | class BugNomination(SQLBase): |
482 | @@ -66,6 +67,9 @@ | |||
483 | 66 | 67 | ||
484 | 67 | def approve(self, approver): | 68 | def approve(self, approver): |
485 | 68 | """See IBugNomination.""" | 69 | """See IBugNomination.""" |
486 | 70 | if self.isApproved(): | ||
487 | 71 | # Approving an approved nomination shouldn't duplicate tasks. | ||
488 | 72 | return | ||
489 | 69 | self.status = BugNominationStatus.APPROVED | 73 | self.status = BugNominationStatus.APPROVED |
490 | 70 | self.decider = approver | 74 | self.decider = approver |
491 | 71 | self.date_decided = datetime.now(pytz.timezone('UTC')) | 75 | self.date_decided = datetime.now(pytz.timezone('UTC')) |
492 | @@ -91,6 +95,9 @@ | |||
493 | 91 | 95 | ||
494 | 92 | def decline(self, decliner): | 96 | def decline(self, decliner): |
495 | 93 | """See IBugNomination.""" | 97 | """See IBugNomination.""" |
496 | 98 | if self.isApproved(): | ||
497 | 99 | raise BugNominationStatusError( | ||
498 | 100 | "Cannot decline an approved nomination.") | ||
499 | 94 | self.status = BugNominationStatus.DECLINED | 101 | self.status = BugNominationStatus.DECLINED |
500 | 95 | self.decider = decliner | 102 | self.decider = decliner |
501 | 96 | self.date_decided = datetime.now(pytz.timezone('UTC')) | 103 | self.date_decided = datetime.now(pytz.timezone('UTC')) |
502 | 97 | 104 | ||
503 | === modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt' | |||
504 | --- lib/lp/bugs/stories/webservice/xx-bug.txt 2009-08-13 15:12:16 +0000 | |||
505 | +++ lib/lp/bugs/stories/webservice/xx-bug.txt 2009-08-26 03:56:55 +0000 | |||
506 | @@ -562,6 +562,213 @@ | |||
507 | 562 | "id": ... "title": "Test bug"... | 562 | "id": ... "title": "Test bug"... |
508 | 563 | 563 | ||
509 | 564 | 564 | ||
510 | 565 | Bug nominations | ||
511 | 566 | --------------- | ||
512 | 567 | |||
513 | 568 | A bug may be nominated for any number of distro or product series. | ||
514 | 569 | Nominations can be inspected, created, approved and declined through | ||
515 | 570 | the webservice. | ||
516 | 571 | |||
517 | 572 | First we'll create Fooix 0.1 and 0.2. Eric is the owner. | ||
518 | 573 | |||
519 | 574 | >>> login('foo.bar@canonical.com') | ||
520 | 575 | >>> eric = factory.makePerson(name='eric') | ||
521 | 576 | >>> fooix = factory.makeProduct(name='fooix', owner=eric) | ||
522 | 577 | >>> fx01 = fooix.newSeries(eric, '0.1', 'The 0.1.x series') | ||
523 | 578 | >>> fx02 = fooix.newSeries(eric, '0.2', 'The 0.2.x series') | ||
524 | 579 | >>> debuntu = factory.makeDistribution(name='debuntu', owner=eric) | ||
525 | 580 | >>> debuntu50 = debuntu.newSeries( | ||
526 | 581 | ... '5.0', '5.0', '5.0', '5.0', '5.0', '5.0', None, eric) | ||
527 | 582 | >>> bug = factory.makeBug(product=fooix) | ||
528 | 583 | >>> logout() | ||
529 | 584 | |||
530 | 585 | Initially there are no nominations. | ||
531 | 586 | |||
532 | 587 | >>> pprint_collection(webservice.named_get( | ||
533 | 588 | ... '/bugs/%d' % bug.id, 'getNominations').jsonBody()) | ||
534 | 589 | start: None | ||
535 | 590 | total_size: 0 | ||
536 | 591 | --- | ||
537 | 592 | |||
538 | 593 | >>> login('foo.bar@canonical.com') | ||
539 | 594 | >>> john = factory.makePerson(name='john') | ||
540 | 595 | >>> logout() | ||
541 | 596 | |||
542 | 597 | >>> from canonical.launchpad.testing.pages import webservice_for_person | ||
543 | 598 | >>> from canonical.launchpad.webapp.interfaces import OAuthPermission | ||
544 | 599 | |||
545 | 600 | >>> john_webservice = webservice_for_person( | ||
546 | 601 | ... john, permission=OAuthPermission.WRITE_PRIVATE) | ||
547 | 602 | |||
548 | 603 | But John, an unprivileged user, wants it fixed in Fooix 0.1.1. | ||
549 | 604 | |||
550 | 605 | >>> print john_webservice.named_post( | ||
551 | 606 | ... '/bugs/%d' % bug.id, 'addNomination', | ||
552 | 607 | ... target=john_webservice.getAbsoluteUrl('/fooix/0.1')) | ||
553 | 608 | HTTP/1.1 201 Created | ||
554 | 609 | ... | ||
555 | 610 | Location: http://.../bugs/.../nominations/... | ||
556 | 611 | ... | ||
557 | 612 | |||
558 | 613 | >>> nominations = webservice.named_get( | ||
559 | 614 | ... '/bugs/%d' % bug.id, 'getNominations').jsonBody() | ||
560 | 615 | >>> pprint_collection(nominations) | ||
561 | 616 | start: 0 | ||
562 | 617 | total_size: 1 | ||
563 | 618 | --- | ||
564 | 619 | bug_link: u'http://.../bugs/...' | ||
565 | 620 | date_created: u'...' | ||
566 | 621 | date_decided: None | ||
567 | 622 | decider_link: None | ||
568 | 623 | distroseries_link: None | ||
569 | 624 | owner_link: u'http://.../~john' | ||
570 | 625 | productseries_link: u'http://.../fooix/0.1' | ||
571 | 626 | resource_type_link: u'http://.../#bug_nomination' | ||
572 | 627 | self_link: u'http://.../bugs/.../nominations/...' | ||
573 | 628 | status: u'Nominated' | ||
574 | 629 | target_link: u'http://.../fooix/0.1' | ||
575 | 630 | --- | ||
576 | 631 | |||
577 | 632 | John cannot approve or decline the nomination. | ||
578 | 633 | |||
579 | 634 | >>> nom_url = nominations['entries'][0]['self_link'] | ||
580 | 635 | |||
581 | 636 | >>> print john_webservice.named_get( | ||
582 | 637 | ... nom_url, 'canApprove', | ||
583 | 638 | ... person=john_webservice.getAbsoluteUrl('/~john')).jsonBody() | ||
584 | 639 | False | ||
585 | 640 | |||
586 | 641 | >>> print john_webservice.named_post(nom_url, 'approve') | ||
587 | 642 | HTTP/1.1 401 Unauthorized... | ||
588 | 643 | |||
589 | 644 | >>> print john_webservice.named_post(nom_url, 'decline') | ||
590 | 645 | HTTP/1.1 401 Unauthorized... | ||
591 | 646 | |||
592 | 647 | >>> login('foo.bar@canonical.com') | ||
593 | 648 | >>> len(bug.bugtasks) | ||
594 | 649 | 1 | ||
595 | 650 | >>> logout() | ||
596 | 651 | |||
597 | 652 | The owner, however, can decline the nomination. | ||
598 | 653 | |||
599 | 654 | >>> print john_webservice.named_get( | ||
600 | 655 | ... nom_url, 'canApprove', | ||
601 | 656 | ... person=john_webservice.getAbsoluteUrl('/~eric')).jsonBody() | ||
602 | 657 | True | ||
603 | 658 | |||
604 | 659 | >>> eric_webservice = webservice_for_person( | ||
605 | 660 | ... eric, permission=OAuthPermission.WRITE_PRIVATE) | ||
606 | 661 | >>> print eric_webservice.named_post(nom_url, 'decline') | ||
607 | 662 | HTTP/1.1 200 Ok... | ||
608 | 663 | |||
609 | 664 | >>> login('foo.bar@canonical.com') | ||
610 | 665 | >>> len(bug.bugtasks) | ||
611 | 666 | 1 | ||
612 | 667 | >>> logout() | ||
613 | 668 | |||
614 | 669 | We can see that the nomination was declined. | ||
615 | 670 | |||
616 | 671 | >>> nominations = webservice.named_get( | ||
617 | 672 | ... '/bugs/%d' % bug.id, 'getNominations').jsonBody() | ||
618 | 673 | >>> pprint_collection(nominations) | ||
619 | 674 | start: 0 | ||
620 | 675 | total_size: 1 | ||
621 | 676 | --- | ||
622 | 677 | bug_link: u'http://.../bugs/...' | ||
623 | 678 | date_created: u'...' | ||
624 | 679 | date_decided: u'...' | ||
625 | 680 | decider_link: u'http://.../~eric' | ||
626 | 681 | distroseries_link: None | ||
627 | 682 | owner_link: u'http://.../~john' | ||
628 | 683 | productseries_link: u'http://.../fooix/0.1' | ||
629 | 684 | resource_type_link: u'http://.../#bug_nomination' | ||
630 | 685 | self_link: u'http://.../bugs/.../nominations/...' | ||
631 | 686 | status: u'Declined' | ||
632 | 687 | target_link: u'http://.../fooix/0.1' | ||
633 | 688 | --- | ||
634 | 689 | |||
635 | 690 | The owner can approve a previously declined nomination. | ||
636 | 691 | |||
637 | 692 | >>> print eric_webservice.named_post(nom_url, 'approve') | ||
638 | 693 | HTTP/1.1 200 Ok... | ||
639 | 694 | |||
640 | 695 | This marks the nomination as Approved, and creates a new task. | ||
641 | 696 | |||
642 | 697 | >>> nominations = webservice.named_get( | ||
643 | 698 | ... '/bugs/%d' % bug.id, 'getNominations').jsonBody() | ||
644 | 699 | >>> pprint_collection(nominations) | ||
645 | 700 | start: 0 | ||
646 | 701 | total_size: 1 | ||
647 | 702 | --- | ||
648 | 703 | bug_link: u'http://.../bugs/...' | ||
649 | 704 | date_created: u'...' | ||
650 | 705 | date_decided: u'...' | ||
651 | 706 | decider_link: u'http://.../~eric' | ||
652 | 707 | distroseries_link: None | ||
653 | 708 | owner_link: u'http://.../~john' | ||
654 | 709 | productseries_link: u'http://.../fooix/0.1' | ||
655 | 710 | resource_type_link: u'http://.../#bug_nomination' | ||
656 | 711 | self_link: u'http://.../bugs/.../nominations/...' | ||
657 | 712 | status: u'Approved' | ||
658 | 713 | target_link: u'http://.../fooix/0.1' | ||
659 | 714 | --- | ||
660 | 715 | |||
661 | 716 | >>> login('foo.bar@canonical.com') | ||
662 | 717 | >>> len(bug.bugtasks) | ||
663 | 718 | 2 | ||
664 | 719 | >>> logout() | ||
665 | 720 | |||
666 | 721 | The owner cannot change his mind and decline an approved task. | ||
667 | 722 | |||
668 | 723 | >>> print eric_webservice.named_post(nom_url, 'decline') | ||
669 | 724 | HTTP/1.1 400 Bad Request | ||
670 | 725 | ... | ||
671 | 726 | Cannot decline an approved nomination. | ||
672 | 727 | |||
673 | 728 | >>> login('foo.bar@canonical.com') | ||
674 | 729 | >>> len(bug.bugtasks) | ||
675 | 730 | 2 | ||
676 | 731 | >>> logout() | ||
677 | 732 | |||
678 | 733 | While he can approve it again, it's a no-op. | ||
679 | 734 | |||
680 | 735 | >>> print eric_webservice.named_post(nom_url, 'approve') | ||
681 | 736 | HTTP/1.1 200 Ok... | ||
682 | 737 | |||
683 | 738 | >>> login('foo.bar@canonical.com') | ||
684 | 739 | >>> len(bug.bugtasks) | ||
685 | 740 | 2 | ||
686 | 741 | >>> logout() | ||
687 | 742 | |||
688 | 743 | A bug cannot be nominated for a non-series. | ||
689 | 744 | |||
690 | 745 | >>> print john_webservice.named_get( | ||
691 | 746 | ... '/bugs/%d' % bug.id, 'canBeNominatedFor', | ||
692 | 747 | ... target=john_webservice.getAbsoluteUrl('/fooix')).jsonBody() | ||
693 | 748 | False | ||
694 | 749 | |||
695 | 750 | >>> print john_webservice.named_post( | ||
696 | 751 | ... '/bugs/%d' % bug.id, 'addNomination', | ||
697 | 752 | ... target=john_webservice.getAbsoluteUrl('/fooix')) | ||
698 | 753 | HTTP/1.1 400 Bad Request | ||
699 | 754 | ... | ||
700 | 755 | NominationError: This bug cannot be nominated for Fooix. | ||
701 | 756 | |||
702 | 757 | The bug also can't be nominated for Debuntu 5.0, as it has no | ||
703 | 758 | Debuntu tasks. | ||
704 | 759 | |||
705 | 760 | >>> print john_webservice.named_get( | ||
706 | 761 | ... '/bugs/%d' % bug.id, 'canBeNominatedFor', | ||
707 | 762 | ... target=john_webservice.getAbsoluteUrl('/debuntu/5.0')).jsonBody() | ||
708 | 763 | False | ||
709 | 764 | |||
710 | 765 | >>> print john_webservice.named_post( | ||
711 | 766 | ... '/bugs/%d' % bug.id, 'addNomination', | ||
712 | 767 | ... target=john_webservice.getAbsoluteUrl('/debuntu/5.0')) | ||
713 | 768 | HTTP/1.1 400 Bad Request | ||
714 | 769 | ... | ||
715 | 770 | NominationError: This bug cannot be nominated for Debuntu 5.0. | ||
716 | 771 | |||
717 | 565 | Bug subscriptions | 772 | Bug subscriptions |
718 | 566 | ----------------- | 773 | ----------------- |
719 | 567 | 774 | ||
720 | @@ -639,9 +846,6 @@ | |||
721 | 639 | Once we have a member, a web service must be created for that user. | 846 | Once we have a member, a web service must be created for that user. |
722 | 640 | Then, the user can unsubsribe the group from the bug. | 847 | Then, the user can unsubsribe the group from the bug. |
723 | 641 | 848 | ||
724 | 642 | >>> from canonical.launchpad.testing.pages import webservice_for_person | ||
725 | 643 | >>> from canonical.launchpad.webapp.interfaces import OAuthPermission | ||
726 | 644 | |||
727 | 645 | >>> member_webservice = webservice_for_person( | 849 | >>> member_webservice = webservice_for_person( |
728 | 646 | ... ubuntu_team_member, permission=OAuthPermission.WRITE_PRIVATE) | 850 | ... ubuntu_team_member, permission=OAuthPermission.WRITE_PRIVATE) |
729 | 647 | 851 | ||
730 | 648 | 852 | ||
731 | === modified file 'lib/lp/bugs/tests/test_bugchanges.py' | |||
732 | --- lib/lp/bugs/tests/test_bugchanges.py 2009-07-17 18:46:25 +0000 | |||
733 | +++ lib/lp/bugs/tests/test_bugchanges.py 2009-08-24 10:45:09 +0000 | |||
734 | @@ -1209,38 +1209,6 @@ | |||
735 | 1209 | 1209 | ||
736 | 1210 | self.assertRecordedChange(expected_activity=expected_activity) | 1210 | self.assertRecordedChange(expected_activity=expected_activity) |
737 | 1211 | 1211 | ||
738 | 1212 | def test_series_nominated_and_approved(self): | ||
739 | 1213 | # When adding a nomination that is approved automatically, it's | ||
740 | 1214 | # like adding a new bug task for the series directly. | ||
741 | 1215 | product = self.factory.makeProduct(owner=self.user) | ||
742 | 1216 | product.driver = self.user | ||
743 | 1217 | series = self.factory.makeProductSeries(product=product) | ||
744 | 1218 | self.bug.addTask(self.user, product) | ||
745 | 1219 | self.saveOldChanges() | ||
746 | 1220 | |||
747 | 1221 | nomination = self.bug.addNomination(self.user, series) | ||
748 | 1222 | self.assertTrue(nomination.isApproved()) | ||
749 | 1223 | |||
750 | 1224 | expected_activity = { | ||
751 | 1225 | 'person': self.user, | ||
752 | 1226 | 'newvalue': series.bugtargetname, | ||
753 | 1227 | 'whatchanged': 'bug task added', | ||
754 | 1228 | 'newvalue': series.bugtargetname, | ||
755 | 1229 | } | ||
756 | 1230 | |||
757 | 1231 | task_added_notification = { | ||
758 | 1232 | 'person': self.user, | ||
759 | 1233 | 'text': ( | ||
760 | 1234 | '** Also affects: %s\n' | ||
761 | 1235 | ' Importance: Undecided\n' | ||
762 | 1236 | ' Status: New' % ( | ||
763 | 1237 | series.bugtargetname)), | ||
764 | 1238 | } | ||
765 | 1239 | |||
766 | 1240 | self.assertRecordedChange( | ||
767 | 1241 | expected_activity=expected_activity, | ||
768 | 1242 | expected_notification=task_added_notification) | ||
769 | 1243 | |||
770 | 1244 | def test_nomination_approved(self): | 1212 | def test_nomination_approved(self): |
771 | 1245 | # When a nomination is approved, it's like adding a new bug | 1213 | # When a nomination is approved, it's like adding a new bug |
772 | 1246 | # task for the series directly. | 1214 | # task for the series directly. |
773 | 1247 | 1215 | ||
774 | === added file 'lib/lp/bugs/tests/test_bugnomination.py' | |||
775 | --- lib/lp/bugs/tests/test_bugnomination.py 1970-01-01 00:00:00 +0000 | |||
776 | +++ lib/lp/bugs/tests/test_bugnomination.py 2009-08-26 02:17:17 +0000 | |||
777 | @@ -0,0 +1,100 @@ | |||
778 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
779 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
780 | 3 | |||
781 | 4 | """Tests related to bug nominations.""" | ||
782 | 5 | |||
783 | 6 | __metaclass__ = type | ||
784 | 7 | |||
785 | 8 | import unittest | ||
786 | 9 | |||
787 | 10 | from canonical.launchpad.ftests import login, logout | ||
788 | 11 | from canonical.testing import DatabaseFunctionalLayer | ||
789 | 12 | from lp.testing import TestCaseWithFactory | ||
790 | 13 | |||
791 | 14 | |||
792 | 15 | class TestBugCanBeNominatedForProductSeries(TestCaseWithFactory): | ||
793 | 16 | """Test IBug.canBeNominated for IProductSeries nominations.""" | ||
794 | 17 | |||
795 | 18 | layer = DatabaseFunctionalLayer | ||
796 | 19 | |||
797 | 20 | def setUp(self): | ||
798 | 21 | super(TestBugCanBeNominatedForProductSeries, self).setUp() | ||
799 | 22 | login('foo.bar@canonical.com') | ||
800 | 23 | self.eric = self.factory.makePerson(name='eric') | ||
801 | 24 | self.setUpTarget() | ||
802 | 25 | |||
803 | 26 | |||
804 | 27 | def setUpTarget(self): | ||
805 | 28 | self.series = self.factory.makeProductSeries() | ||
806 | 29 | self.bug = self.factory.makeBug(product=self.series.product) | ||
807 | 30 | self.milestone = self.factory.makeMilestone(productseries=self.series) | ||
808 | 31 | self.random_series = self.factory.makeProductSeries() | ||
809 | 32 | |||
810 | 33 | def test_canBeNominatedFor_series(self): | ||
811 | 34 | # A bug may be nominated for a series of a product with an existing | ||
812 | 35 | # task. | ||
813 | 36 | self.assertTrue(self.bug.canBeNominatedFor(self.series)) | ||
814 | 37 | |||
815 | 38 | def test_not_canBeNominatedFor_already_nominated_series(self): | ||
816 | 39 | # A bug may not be nominated for a series with an existing nomination. | ||
817 | 40 | self.assertTrue(self.bug.canBeNominatedFor(self.series)) | ||
818 | 41 | self.bug.addNomination(self.eric, self.series) | ||
819 | 42 | self.assertFalse(self.bug.canBeNominatedFor(self.series)) | ||
820 | 43 | |||
821 | 44 | def test_not_canBeNominatedFor_non_series(self): | ||
822 | 45 | # A bug may not be nominated for something other than a series. | ||
823 | 46 | self.assertFalse(self.bug.canBeNominatedFor(self.milestone)) | ||
824 | 47 | |||
825 | 48 | def test_not_canBeNominatedFor_already_targeted_series(self): | ||
826 | 49 | # A bug may not be nominated for a series if a task already exists. | ||
827 | 50 | # This case should be caught by the check for an existing nomination, | ||
828 | 51 | # but there are some historical cases where a series task exists | ||
829 | 52 | # without a nomination. | ||
830 | 53 | self.assertTrue(self.bug.canBeNominatedFor(self.series)) | ||
831 | 54 | self.bug.addTask(self.eric, self.series) | ||
832 | 55 | self.assertFalse(self.bug.canBeNominatedFor(self.series)) | ||
833 | 56 | |||
834 | 57 | def test_not_canBeNominatedFor_random_series(self): | ||
835 | 58 | # A bug may only be nominated for a series if that series' pillar | ||
836 | 59 | # already has a task. | ||
837 | 60 | self.assertFalse(self.bug.canBeNominatedFor(self.random_series)) | ||
838 | 61 | |||
839 | 62 | def tearDown(self): | ||
840 | 63 | logout() | ||
841 | 64 | super(TestBugCanBeNominatedForProductSeries, self).tearDown() | ||
842 | 65 | |||
843 | 66 | |||
844 | 67 | class TestBugCanBeNominatedForDistroSeries( | ||
845 | 68 | TestBugCanBeNominatedForProductSeries): | ||
846 | 69 | """Test IBug.canBeNominated for IDistroSeries nominations.""" | ||
847 | 70 | |||
848 | 71 | def setUpTarget(self): | ||
849 | 72 | self.series = self.factory.makeDistroRelease() | ||
850 | 73 | # The factory can't create a distro bug directly. | ||
851 | 74 | self.bug = self.factory.makeBug() | ||
852 | 75 | self.bug.addTask(self.eric, self.series.distribution) | ||
853 | 76 | self.milestone = self.factory.makeMilestone( | ||
854 | 77 | distribution=self.series.distribution) | ||
855 | 78 | self.random_series = self.factory.makeDistroRelease() | ||
856 | 79 | |||
857 | 80 | def test_not_canBeNominatedFor_source_package(self): | ||
858 | 81 | # A bug may not be nominated directly for a source package. The | ||
859 | 82 | # distroseries must be nominated instead. | ||
860 | 83 | spn = self.factory.makeSourcePackageName() | ||
861 | 84 | source_package = self.series.getSourcePackage(spn) | ||
862 | 85 | self.assertFalse(self.bug.canBeNominatedFor(source_package)) | ||
863 | 86 | |||
864 | 87 | def test_canBeNominatedFor_with_only_distributionsourcepackage(self): | ||
865 | 88 | # A distribution source package task is sufficient to allow nomination | ||
866 | 89 | # to a series of that distribution. | ||
867 | 90 | sp_bug = self.factory.makeBug() | ||
868 | 91 | spn = self.factory.makeSourcePackageName() | ||
869 | 92 | |||
870 | 93 | self.assertFalse(sp_bug.canBeNominatedFor(self.series)) | ||
871 | 94 | sp_bug.addTask( | ||
872 | 95 | self.eric, self.series.distribution.getSourcePackage(spn)) | ||
873 | 96 | self.assertTrue(sp_bug.canBeNominatedFor(self.series)) | ||
874 | 97 | |||
875 | 98 | |||
876 | 99 | def test_suite(): | ||
877 | 100 | return unittest.TestLoader().loadTestsFromName(__name__) |
= Summary =
Bug nominations want to be exported through the API.
== Proposed fix ==
Export IBugNomination and relevant methods from IBug, moving access control code from views to model classes.
== Implementation details ==
All of the relevant IBug and IBugNomination methods already took the user as an argument, so no signature changes were required.
The main non-trivial bits are the refactoring of IBug.canBeNomin atedFor and IBug.addNomination to properly forbid attempts to nominate a non-series. Rather than introduce an ISeriesBugTarget as suggested in the bug, I opted to just use IBugTarget. Further type checking was going to be required anyway when dealing with an ISeriesBugTarget, so it's probably cleaner this way.
There are four significant model behaviour changes: approve is now a no-op if it is already approved, as IBugNomination says. Previously it would crash or create a duplicate task. decline now fails if it is already approved, as IBugNomination says. Previously it would successfully decline the task.
- IBug.addNomination will no longer automatically approve nominations if possible. The view does this explicitly instead.
- BugNomination.
- BugNomination.
- Bug.addNomination will refuse to create a nomination for a series on a pillar that does not already have a task. This has always been enforced in the view.
== Tests == atedFor and the webservice, and a test to verify that nominations are automatically approved was dropped. Other tests required trivial changes to comply with the new model restrictions.
Direct tests were added for IBug.canBeNomin
$ bin/test -t test_bugnomination -t bugtask- edit-views. txt -t bug-nomination.txt -t bug-set-status.txt -t bugtask.txt -t xx-bug.txt -t test_bugchanges