Merge lp:~allenap/launchpad/sub-filters-via-api-bug-672619 into lp:launchpad
- sub-filters-via-api-bug-672619
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12132 |
Proposed branch: | lp:~allenap/launchpad/sub-filters-via-api-bug-672619 |
Merge into: | lp:launchpad |
Diff against target: |
727 lines (+373/-51) 12 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+2/-0) lib/lp/bugs/browser/configure.zcml (+9/-0) lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py (+210/-0) lib/lp/bugs/interfaces/bugsubscriptionfilter.py (+44/-28) lib/lp/bugs/interfaces/webservice.py (+6/-1) lib/lp/registry/browser/configure.zcml (+3/-0) lib/lp/registry/browser/structuralsubscription.py (+19/-3) lib/lp/registry/browser/tests/test_structuralsubscription.py (+53/-1) lib/lp/registry/interfaces/structuralsubscription.py (+3/-2) lib/lp/registry/model/structuralsubscription.py (+2/-0) lib/lp/registry/stories/webservice/xx-structuralsubscription.txt (+3/-0) utilities/format-imports (+19/-16) |
To merge this branch: | bzr merge lp:~allenap/launchpad/sub-filters-via-api-bug-672619 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Deryck Hodge (community) | code | Approve | |
Review via email: mp+44331@code.launchpad.net |
Commit message
[r=deryck]
Description of the change
This exposes bug subscription filters to the API.
There are a few parts to making this happen:
- Providing URL information for bug subscription filters:
lib/lp/
- Providing navigation for bug subscription filters:
lib/lp/
lib/lp/
for TestBugSubscrip
lib/lp/
for StructuralSubsc
- Exporting IStructuralSubs
lib/canonical
lib/lp/
lib/lp/
- Exporting IBugSubscriptio
lib/lp/
for TestBugSubscrip
for TestBugSubscrip
lib/lp/
lib/lp/
- Fix a weird issue where navigation was attempted on a non-flushed
object. Haven't investigated this, but a flush prevents issues.
lib/lp/
Gavin Panella (allenap) wrote : | # |
Deryck Hodge (deryck) wrote : | # |
Looks fine to me, per our discussions on IRC.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py' |
2 | --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-01 19:12:00 +0000 |
3 | +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-21 23:23:17 +0000 |
4 | @@ -420,6 +420,8 @@ |
5 | # IStructuralSubscription |
6 | patch_collection_property( |
7 | IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter) |
8 | +patch_entry_return_type( |
9 | + IStructuralSubscription, "newBugFilter", IBugSubscriptionFilter) |
10 | patch_reference_property( |
11 | IStructuralSubscription, 'target', IStructuralSubscriptionTarget) |
12 | |
13 | |
14 | === modified file 'lib/lp/bugs/browser/configure.zcml' |
15 | --- lib/lp/bugs/browser/configure.zcml 2010-10-28 09:11:36 +0000 |
16 | +++ lib/lp/bugs/browser/configure.zcml 2010-12-21 23:23:17 +0000 |
17 | @@ -1176,4 +1176,13 @@ |
18 | BugWatchSetNavigation"/> |
19 | </facet> |
20 | |
21 | + <!-- Bug Subscription Filters --> |
22 | + <facet facet="bugs"> |
23 | + <browser:url |
24 | + for="lp.bugs.interfaces.bugsubscriptionfilter.IBugSubscriptionFilter" |
25 | + path_expression="string:+filter/${id}" |
26 | + attribute_to_parent="structural_subscription" |
27 | + rootsite="bugs" /> |
28 | + </facet> |
29 | + |
30 | </configure> |
31 | |
32 | === added file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py' |
33 | --- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000 |
34 | +++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000 |
35 | @@ -0,0 +1,210 @@ |
36 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
37 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
38 | + |
39 | +"""Tests for bug subscription filter browser code.""" |
40 | + |
41 | +__metaclass__ = type |
42 | + |
43 | +from functools import partial |
44 | +from urlparse import urlparse |
45 | + |
46 | +from lazr.restfulclient.errors import BadRequest |
47 | +from storm.exceptions import LostObjectError |
48 | +from testtools.matchers import StartsWith |
49 | +import transaction |
50 | + |
51 | +from canonical.launchpad.webapp.publisher import canonical_url |
52 | +from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
53 | +from canonical.testing.layers import ( |
54 | + AppServerLayer, |
55 | + LaunchpadFunctionalLayer, |
56 | + ) |
57 | +from lp.bugs.interfaces.bugtask import ( |
58 | + BugTaskImportance, |
59 | + BugTaskStatus, |
60 | + ) |
61 | +from lp.registry.browser.structuralsubscription import ( |
62 | + StructuralSubscriptionNavigation, |
63 | + ) |
64 | +from lp.testing import ( |
65 | + person_logged_in, |
66 | + TestCaseWithFactory, |
67 | + ws_object, |
68 | + ) |
69 | + |
70 | + |
71 | +class TestBugSubscriptionFilterBase: |
72 | + |
73 | + def setUp(self): |
74 | + super(TestBugSubscriptionFilterBase, self).setUp() |
75 | + self.owner = self.factory.makePerson(name=u"foo") |
76 | + self.structure = self.factory.makeProduct( |
77 | + owner=self.owner, name=u"bar") |
78 | + with person_logged_in(self.owner): |
79 | + self.subscription = self.structure.addBugSubscription( |
80 | + self.owner, self.owner) |
81 | + self.subscription_filter = self.subscription.newBugFilter() |
82 | + |
83 | + |
84 | +class TestBugSubscriptionFilterNavigation( |
85 | + TestBugSubscriptionFilterBase, TestCaseWithFactory): |
86 | + |
87 | + layer = LaunchpadFunctionalLayer |
88 | + |
89 | + def test_canonical_url(self): |
90 | + url = urlparse(canonical_url(self.subscription_filter)) |
91 | + self.assertThat(url.hostname, StartsWith("bugs.")) |
92 | + self.assertEqual( |
93 | + "/bar/+subscription/foo/+filter/%d" % ( |
94 | + self.subscription_filter.id), |
95 | + url.path) |
96 | + |
97 | + def test_navigation(self): |
98 | + request = LaunchpadTestRequest() |
99 | + request.setTraversalStack([unicode(self.subscription_filter.id)]) |
100 | + navigation = StructuralSubscriptionNavigation( |
101 | + self.subscription, request) |
102 | + view = navigation.publishTraverse(request, '+filter') |
103 | + self.assertIsNot(None, view) |
104 | + |
105 | + |
106 | +class TestBugSubscriptionFilterAPI( |
107 | + TestBugSubscriptionFilterBase, TestCaseWithFactory): |
108 | + |
109 | + layer = AppServerLayer |
110 | + |
111 | + def test_visible_attributes(self): |
112 | + # Bug subscription filters are not private objects. All attributes are |
113 | + # visible to everyone. |
114 | + transaction.commit() |
115 | + # Create a service for a new person. |
116 | + service = self.factory.makeLaunchpadService() |
117 | + get_ws_object = partial(ws_object, service) |
118 | + ws_subscription = get_ws_object(self.subscription) |
119 | + ws_subscription_filter = get_ws_object(self.subscription_filter) |
120 | + self.assertEqual( |
121 | + ws_subscription.self_link, |
122 | + ws_subscription_filter.structural_subscription_link) |
123 | + self.assertEqual( |
124 | + self.subscription_filter.find_all_tags, |
125 | + ws_subscription_filter.find_all_tags) |
126 | + self.assertEqual( |
127 | + self.subscription_filter.description, |
128 | + ws_subscription_filter.description) |
129 | + self.assertEqual( |
130 | + list(self.subscription_filter.statuses), |
131 | + ws_subscription_filter.statuses) |
132 | + self.assertEqual( |
133 | + list(self.subscription_filter.importances), |
134 | + ws_subscription_filter.importances) |
135 | + self.assertEqual( |
136 | + list(self.subscription_filter.tags), |
137 | + ws_subscription_filter.tags) |
138 | + |
139 | + def test_structural_subscription_cannot_be_modified(self): |
140 | + # Bug filters cannot be moved from one structural subscription to |
141 | + # another. In other words, the structural_subscription field is |
142 | + # read-only. |
143 | + user = self.factory.makePerson(name=u"baz") |
144 | + with person_logged_in(self.owner): |
145 | + user_subscription = self.structure.addBugSubscription(user, user) |
146 | + transaction.commit() |
147 | + # Create a service for the structure owner. |
148 | + service = self.factory.makeLaunchpadService(self.owner) |
149 | + get_ws_object = partial(ws_object, service) |
150 | + ws_user_subscription = get_ws_object(user_subscription) |
151 | + ws_subscription_filter = get_ws_object(self.subscription_filter) |
152 | + ws_subscription_filter.structural_subscription = ws_user_subscription |
153 | + error = self.assertRaises(BadRequest, ws_subscription_filter.lp_save) |
154 | + self.assertEqual(400, error.response.status) |
155 | + self.assertEqual( |
156 | + self.subscription, |
157 | + self.subscription_filter.structural_subscription) |
158 | + |
159 | + |
160 | +class TestBugSubscriptionFilterAPIModifications( |
161 | + TestBugSubscriptionFilterBase, TestCaseWithFactory): |
162 | + |
163 | + layer = AppServerLayer |
164 | + |
165 | + def setUp(self): |
166 | + super(TestBugSubscriptionFilterAPIModifications, self).setUp() |
167 | + transaction.commit() |
168 | + self.service = self.factory.makeLaunchpadService(self.owner) |
169 | + self.ws_subscription_filter = ws_object( |
170 | + self.service, self.subscription_filter) |
171 | + |
172 | + def test_modify_tags_fields(self): |
173 | + # Two tags-related fields - find_all_tags and tags - can be |
174 | + # modified. The other two tags-related fields - include_any_tags and |
175 | + # exclude_any_tags - are not exported because the tags field provides |
176 | + # a more intuitive way to update them (from the perspective of an API |
177 | + # consumer). |
178 | + self.assertFalse(self.subscription_filter.find_all_tags) |
179 | + self.assertFalse(self.subscription_filter.include_any_tags) |
180 | + self.assertFalse(self.subscription_filter.exclude_any_tags) |
181 | + self.assertEqual(set(), self.subscription_filter.tags) |
182 | + |
183 | + # Modify, save, and start a new transaction. |
184 | + self.ws_subscription_filter.find_all_tags = True |
185 | + self.ws_subscription_filter.tags = ["foo", "-bar", "*", "-*"] |
186 | + self.ws_subscription_filter.lp_save() |
187 | + transaction.begin() |
188 | + |
189 | + # Updated state. |
190 | + self.assertTrue(self.subscription_filter.find_all_tags) |
191 | + self.assertTrue(self.subscription_filter.include_any_tags) |
192 | + self.assertTrue(self.subscription_filter.exclude_any_tags) |
193 | + self.assertEqual( |
194 | + set(["*", "-*", "foo", "-bar"]), |
195 | + self.subscription_filter.tags) |
196 | + |
197 | + def test_modify_description(self): |
198 | + # The description can be modified. |
199 | + self.assertEqual( |
200 | + None, self.subscription_filter.description) |
201 | + |
202 | + # Modify, save, and start a new transaction. |
203 | + self.ws_subscription_filter.description = u"It's late." |
204 | + self.ws_subscription_filter.lp_save() |
205 | + transaction.begin() |
206 | + |
207 | + # Updated state. |
208 | + self.assertEqual( |
209 | + u"It's late.", self.subscription_filter.description) |
210 | + |
211 | + def test_modify_statuses(self): |
212 | + # The statuses field can be modified. |
213 | + self.assertEqual(set(), self.subscription_filter.statuses) |
214 | + |
215 | + # Modify, save, and start a new transaction. |
216 | + self.ws_subscription_filter.statuses = ["New", "Triaged"] |
217 | + self.ws_subscription_filter.lp_save() |
218 | + transaction.begin() |
219 | + |
220 | + # Updated state. |
221 | + self.assertEqual( |
222 | + set([BugTaskStatus.NEW, BugTaskStatus.TRIAGED]), |
223 | + self.subscription_filter.statuses) |
224 | + |
225 | + def test_modify_importances(self): |
226 | + # The importances field can be modified. |
227 | + self.assertEqual(set(), self.subscription_filter.importances) |
228 | + |
229 | + # Modify, save, and start a new transaction. |
230 | + self.ws_subscription_filter.importances = ["Low", "High"] |
231 | + self.ws_subscription_filter.lp_save() |
232 | + transaction.begin() |
233 | + |
234 | + # Updated state. |
235 | + self.assertEqual( |
236 | + set([BugTaskImportance.LOW, BugTaskImportance.HIGH]), |
237 | + self.subscription_filter.importances) |
238 | + |
239 | + def test_delete(self): |
240 | + # Subscription filters can be deleted. |
241 | + self.ws_subscription_filter.lp_delete() |
242 | + transaction.begin() |
243 | + self.assertRaises( |
244 | + LostObjectError, getattr, self.subscription_filter, |
245 | + "find_all_tags") |
246 | |
247 | === modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py' |
248 | --- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-10-04 13:24:54 +0000 |
249 | +++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000 |
250 | @@ -9,12 +9,18 @@ |
251 | ] |
252 | |
253 | |
254 | +from lazr.restful.declarations import ( |
255 | + export_as_webservice_entry, |
256 | + export_destructor_operation, |
257 | + exported, |
258 | + ) |
259 | from lazr.restful.fields import Reference |
260 | from zope.interface import Interface |
261 | from zope.schema import ( |
262 | Bool, |
263 | Choice, |
264 | FrozenSet, |
265 | + Int, |
266 | Text, |
267 | ) |
268 | |
269 | @@ -32,14 +38,18 @@ |
270 | class IBugSubscriptionFilterAttributes(Interface): |
271 | """Attributes of `IBugSubscriptionFilter`.""" |
272 | |
273 | - structural_subscription = Reference( |
274 | - IStructuralSubscription, |
275 | - title=_("Structural subscription"), |
276 | - required=True, readonly=True) |
277 | - |
278 | - find_all_tags = Bool( |
279 | - title=_("All given tags must be found, or any."), |
280 | - required=True, default=False) |
281 | + id = Int(required=True, readonly=True) |
282 | + |
283 | + structural_subscription = exported( |
284 | + Reference( |
285 | + IStructuralSubscription, |
286 | + title=_("Structural subscription"), |
287 | + required=True, readonly=True)) |
288 | + |
289 | + find_all_tags = exported( |
290 | + Bool( |
291 | + title=_("All given tags must be found, or any."), |
292 | + required=True, default=False)) |
293 | include_any_tags = Bool( |
294 | title=_("Include any tags."), |
295 | required=True, default=False) |
296 | @@ -47,31 +57,36 @@ |
297 | title=_("Exclude all tags."), |
298 | required=True, default=False) |
299 | |
300 | - description = Text( |
301 | - title=_("Description of this filter."), |
302 | - required=False) |
303 | - |
304 | - statuses = FrozenSet( |
305 | - title=_("The statuses to filter on."), |
306 | - required=True, default=frozenset(), |
307 | - value_type=Choice( |
308 | - title=_('Status'), vocabulary=BugTaskStatus)) |
309 | - |
310 | - importances = FrozenSet( |
311 | - title=_("The importances to filter on."), |
312 | - required=True, default=frozenset(), |
313 | - value_type=Choice( |
314 | - title=_('Importance'), vocabulary=BugTaskImportance)) |
315 | - |
316 | - tags = FrozenSet( |
317 | - title=_("The tags to filter on."), |
318 | - required=True, default=frozenset(), |
319 | - value_type=SearchTag()) |
320 | + description = exported( |
321 | + Text( |
322 | + title=_("Description of this filter."), |
323 | + required=False)) |
324 | + |
325 | + statuses = exported( |
326 | + FrozenSet( |
327 | + title=_("The statuses to filter on."), |
328 | + required=True, default=frozenset(), |
329 | + value_type=Choice( |
330 | + title=_('Status'), vocabulary=BugTaskStatus))) |
331 | + |
332 | + importances = exported( |
333 | + FrozenSet( |
334 | + title=_("The importances to filter on."), |
335 | + required=True, default=frozenset(), |
336 | + value_type=Choice( |
337 | + title=_('Importance'), vocabulary=BugTaskImportance))) |
338 | + |
339 | + tags = exported( |
340 | + FrozenSet( |
341 | + title=_("The tags to filter on."), |
342 | + required=True, default=frozenset(), |
343 | + value_type=SearchTag())) |
344 | |
345 | |
346 | class IBugSubscriptionFilterMethods(Interface): |
347 | """Methods of `IBugSubscriptionFilter`.""" |
348 | |
349 | + @export_destructor_operation() |
350 | def delete(): |
351 | """Delete this bug subscription filter.""" |
352 | |
353 | @@ -79,3 +94,4 @@ |
354 | class IBugSubscriptionFilter( |
355 | IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods): |
356 | """A bug subscription filter.""" |
357 | + export_as_webservice_entry() |
358 | |
359 | === modified file 'lib/lp/bugs/interfaces/webservice.py' |
360 | --- lib/lp/bugs/interfaces/webservice.py 2010-12-01 22:18:07 +0000 |
361 | +++ lib/lp/bugs/interfaces/webservice.py 2010-12-21 23:23:17 +0000 |
362 | @@ -50,7 +50,6 @@ |
363 | from lp.bugs.interfaces.bugattachment import IBugAttachment |
364 | from lp.bugs.interfaces.bugbranch import IBugBranch |
365 | from lp.bugs.interfaces.buglink import IBugLinkTarget |
366 | -from lp.bugs.interfaces.malone import IMaloneApplication |
367 | from lp.bugs.interfaces.bugnomination import ( |
368 | BugNominationStatusError, |
369 | IBugNomination, |
370 | @@ -58,6 +57,7 @@ |
371 | NominationSeriesObsoleteError, |
372 | ) |
373 | from lp.bugs.interfaces.bugsubscription import IBugSubscription |
374 | +from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter |
375 | from lp.bugs.interfaces.bugtarget import ( |
376 | IBugTarget, |
377 | IHasBugs, |
378 | @@ -82,6 +82,11 @@ |
379 | ICve, |
380 | ICveSet, |
381 | ) |
382 | +from lp.bugs.interfaces.malone import IMaloneApplication |
383 | + |
384 | + |
385 | + |
386 | |
387 | + |
388 | # XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular |
389 | # import bugs. Break this up into a per-package thing. |
390 | from canonical.launchpad.interfaces import _schema_circular_imports |
391 | |
392 | === modified file 'lib/lp/registry/browser/configure.zcml' |
393 | --- lib/lp/registry/browser/configure.zcml 2010-12-15 22:05:43 +0000 |
394 | +++ lib/lp/registry/browser/configure.zcml 2010-12-21 23:23:17 +0000 |
395 | @@ -2225,6 +2225,9 @@ |
396 | for="lp.registry.interfaces.structuralsubscription.IStructuralSubscription" |
397 | path_expression="string:+subscription/${subscriber/name}" |
398 | attribute_to_parent="target"/> |
399 | + <browser:navigation |
400 | + module="lp.registry.browser.structuralsubscription" |
401 | + classes="StructuralSubscriptionNavigation"/> |
402 | |
403 | <browser:url |
404 | for="lp.registry.interfaces.personproduct.IPersonProduct" |
405 | |
406 | === modified file 'lib/lp/registry/browser/structuralsubscription.py' |
407 | --- lib/lp/registry/browser/structuralsubscription.py 2010-11-29 12:25:43 +0000 |
408 | +++ lib/lp/registry/browser/structuralsubscription.py 2010-12-21 23:23:17 +0000 |
409 | @@ -23,13 +23,14 @@ |
410 | SimpleVocabulary, |
411 | ) |
412 | |
413 | -from canonical.launchpad.webapp import ( |
414 | +from canonical.launchpad.webapp.authorization import check_permission |
415 | +from canonical.launchpad.webapp.menu import Link |
416 | +from canonical.launchpad.webapp.publisher import ( |
417 | canonical_url, |
418 | LaunchpadView, |
419 | + Navigation, |
420 | stepthrough, |
421 | ) |
422 | -from canonical.launchpad.webapp.authorization import check_permission |
423 | -from canonical.launchpad.webapp.menu import Link |
424 | from canonical.widgets import LabeledMultiCheckBoxWidget |
425 | from lp.app.browser.launchpadform import ( |
426 | action, |
427 | @@ -44,14 +45,29 @@ |
428 | from lp.registry.interfaces.milestone import IProjectGroupMilestone |
429 | from lp.registry.interfaces.person import IPersonSet |
430 | from lp.registry.interfaces.structuralsubscription import ( |
431 | + IStructuralSubscription, |
432 | IStructuralSubscriptionForm, |
433 | IStructuralSubscriptionTarget, |
434 | ) |
435 | from lp.services.propertycache import cachedproperty |
436 | |
437 | |
438 | +class StructuralSubscriptionNavigation(Navigation): |
439 | + |
440 | + usedfor = IStructuralSubscription |
441 | + |
442 | + @stepthrough("+filter") |
443 | + def bug_filter(self, filter_id): |
444 | + bug_filter_id = int(filter_id) |
445 | + for bug_filter in self.context.bug_filters: |
446 | + if bug_filter.id == bug_filter_id: |
447 | + return bug_filter |
448 | + return None |
449 | + |
450 | + |
451 | class StructuralSubscriptionView(LaunchpadFormView, |
452 | AdvancedSubscriptionMixin): |
453 | + |
454 | """View class for structural subscriptions.""" |
455 | |
456 | schema = IStructuralSubscriptionForm |
457 | |
458 | === modified file 'lib/lp/registry/browser/tests/test_structuralsubscription.py' |
459 | --- lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-11-30 11:38:12 +0000 |
460 | +++ lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-12-21 23:23:17 +0000 |
461 | @@ -3,7 +3,10 @@ |
462 | |
463 | """Tests for structural subscription traversal.""" |
464 | |
465 | +from urlparse import urlparse |
466 | + |
467 | from lazr.restful.testing.webservice import FakeRequest |
468 | +import transaction |
469 | from zope.publisher.interfaces import NotFound |
470 | |
471 | from canonical.launchpad.ftests import ( |
472 | @@ -14,6 +17,7 @@ |
473 | from canonical.launchpad.webapp.publisher import canonical_url |
474 | from canonical.launchpad.webapp.servers import StepsToGo |
475 | from canonical.testing.layers import ( |
476 | + AppServerLayer, |
477 | DatabaseFunctionalLayer, |
478 | LaunchpadFunctionalLayer, |
479 | ) |
480 | @@ -27,13 +31,15 @@ |
481 | from lp.registry.browser.productseries import ProductSeriesNavigation |
482 | from lp.registry.browser.project import ProjectNavigation |
483 | from lp.registry.browser.structuralsubscription import ( |
484 | - StructuralSubscriptionView) |
485 | + StructuralSubscriptionView, |
486 | + ) |
487 | from lp.registry.enum import BugNotificationLevel |
488 | from lp.testing import ( |
489 | feature_flags, |
490 | person_logged_in, |
491 | set_feature_flag, |
492 | TestCaseWithFactory, |
493 | + ws_object, |
494 | ) |
495 | from lp.testing.views import create_initialized_view |
496 | |
497 | @@ -356,3 +362,49 @@ |
498 | self.assertEqual( |
499 | "To all bugs in %s" % self.target.displayname, |
500 | self.view.target_label) |
501 | + |
502 | + |
503 | +class TestStructuralSubscriptionAPI(TestCaseWithFactory): |
504 | + |
505 | + layer = AppServerLayer |
506 | + |
507 | + def setUp(self): |
508 | + super(TestStructuralSubscriptionAPI, self).setUp() |
509 | + self.owner = self.factory.makePerson(name=u"foo") |
510 | + self.structure = self.factory.makeProduct( |
511 | + owner=self.owner, name=u"bar") |
512 | + with person_logged_in(self.owner): |
513 | + self.subscription = self.structure.addBugSubscription( |
514 | + self.owner, self.owner) |
515 | + transaction.commit() |
516 | + self.service = self.factory.makeLaunchpadService(self.owner) |
517 | + self.ws_subscription = ws_object(self.service, self.subscription) |
518 | + |
519 | + def test_newBugFilter(self): |
520 | + # New bug subscription filters can be created with newBugFilter(). |
521 | + ws_subscription_filter = self.ws_subscription.newBugFilter() |
522 | + self.assertEqual( |
523 | + "bug_subscription_filter", |
524 | + urlparse(ws_subscription_filter.resource_type_link).fragment) |
525 | + self.assertEqual( |
526 | + ws_subscription_filter.structural_subscription.self_link, |
527 | + self.ws_subscription.self_link) |
528 | + |
529 | + def test_bug_filters(self): |
530 | + # The bug_filters property is a collection of IBugSubscriptionFilter |
531 | + # instances previously created by newBugFilter(). |
532 | + bug_filter_links = lambda: set( |
533 | + bug_filter.self_link for bug_filter in ( |
534 | + self.ws_subscription.bug_filters)) |
535 | + self.assertEqual(set(), bug_filter_links()) |
536 | + # A new filter appears in the bug_filters collection. |
537 | + ws_subscription_filter1 = self.ws_subscription.newBugFilter() |
538 | + self.assertEqual( |
539 | + set([ws_subscription_filter1.self_link]), |
540 | + bug_filter_links()) |
541 | + # A second new filter also appears in the bug_filters collection. |
542 | + ws_subscription_filter2 = self.ws_subscription.newBugFilter() |
543 | + self.assertEqual( |
544 | + set([ws_subscription_filter1.self_link, |
545 | + ws_subscription_filter2.self_link]), |
546 | + bug_filter_links()) |
547 | |
548 | === modified file 'lib/lp/registry/interfaces/structuralsubscription.py' |
549 | --- lib/lp/registry/interfaces/structuralsubscription.py 2010-11-09 11:00:55 +0000 |
550 | +++ lib/lp/registry/interfaces/structuralsubscription.py 2010-12-21 23:23:17 +0000 |
551 | @@ -129,11 +129,12 @@ |
552 | required=True, readonly=True, |
553 | title=_("The structure to which this subscription belongs."))) |
554 | |
555 | - bug_filters = CollectionField( |
556 | + bug_filters = exported(CollectionField( |
557 | title=_('List of bug filters that narrow this subscription.'), |
558 | readonly=True, required=False, |
559 | - value_type=Reference(schema=Interface)) |
560 | + value_type=Reference(schema=Interface))) |
561 | |
562 | + @export_factory_operation(Interface, []) |
563 | def newBugFilter(): |
564 | """Returns a new `BugSubscriptionFilter` for this subscription.""" |
565 | |
566 | |
567 | === modified file 'lib/lp/registry/model/structuralsubscription.py' |
568 | --- lib/lp/registry/model/structuralsubscription.py 2010-11-26 17:08:03 +0000 |
569 | +++ lib/lp/registry/model/structuralsubscription.py 2010-12-21 23:23:17 +0000 |
570 | @@ -160,6 +160,8 @@ |
571 | """See `IStructuralSubscription`.""" |
572 | bug_filter = BugSubscriptionFilter() |
573 | bug_filter.structural_subscription = self |
574 | + # This flush is needed for the web service API. |
575 | + IStore(StructuralSubscription).flush() |
576 | return bug_filter |
577 | |
578 | |
579 | |
580 | === modified file 'lib/lp/registry/stories/webservice/xx-structuralsubscription.txt' |
581 | --- lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-08-05 20:49:08 +0000 |
582 | +++ lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-12-21 23:23:17 +0000 |
583 | @@ -46,6 +46,7 @@ |
584 | start: 0 |
585 | total_size: 1 |
586 | --- |
587 | + bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters' |
588 | date_created: u'...' |
589 | date_last_updated: u'...' |
590 | resource_type_link: u'http://.../#structural_subscription' |
591 | @@ -60,6 +61,7 @@ |
592 | >>> pprint_entry(eric_webservice.named_get( |
593 | ... '/fooix', 'getSubscription', |
594 | ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()) |
595 | + bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters' |
596 | date_created: u'...' |
597 | date_last_updated: u'...' |
598 | resource_type_link: u'http://.../#structural_subscription' |
599 | @@ -117,6 +119,7 @@ |
600 | start: 0 |
601 | total_size: 1 |
602 | --- |
603 | + bug_filters_collection_link: u'.../fooix/+subscription/pythons/bug_filters' |
604 | date_created: u'...' |
605 | date_last_updated: u'...' |
606 | resource_type_link: u'http://.../#structural_subscription' |
607 | |
608 | === modified file 'utilities/format-imports' |
609 | --- utilities/format-imports 2010-08-27 20:17:23 +0000 |
610 | +++ utilities/format-imports 2010-12-21 23:23:17 +0000 |
611 | @@ -24,7 +24,7 @@ |
612 | that start with "import" or "from" or are indented with at least one space or |
613 | are blank lines. Comment lines are also included if they are followed by an |
614 | import statement. An inital __future__ import and a module docstring are |
615 | -explicitly skipped. |
616 | +explicitly skipped. |
617 | |
618 | The import section is rewritten as three subsections, each separated by a |
619 | blank line. Any of the sections may be empty. |
620 | @@ -123,7 +123,7 @@ |
621 | from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \ |
622 | import object |
623 | }}} |
624 | -""" |
625 | +""" |
626 | |
627 | __metaclass__ = type |
628 | |
629 | @@ -145,7 +145,8 @@ |
630 | "(?P<objects>[*]|[a-zA-Z0-9_, ]+)" |
631 | "(?P<comment>#.*)?$", re.M) |
632 | from_import_multi_regex = re.compile( |
633 | - "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", re.M) |
634 | + "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", |
635 | + re.M) |
636 | comment_regex = re.compile( |
637 | "(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M) |
638 | split_regex = re.compile(",\s*") |
639 | @@ -153,14 +154,16 @@ |
640 | # Module docstrings are multiline (""") strings that are not indented and are |
641 | # followed at some point by an import . |
642 | module_docstring_regex = re.compile( |
643 | - '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', re.M | re.S) |
644 | + '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', |
645 | + re.M | re.S) |
646 | # The imports section starts with an import state that is not a __future__ |
647 | # import and consists of import lines, indented lines, empty lines and |
648 | # comments which are followed by an import line. Sometimes we even find |
649 | # lines that contain a single ")"... :-( |
650 | imports_section_regex = re.compile( |
651 | "(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n" |
652 | - "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) .+\n)|^\n|^[)]\n)*", |
653 | + "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) " |
654 | + ".+\n)|^\n|^[)]\n)*", |
655 | re.M) |
656 | |
657 | |
658 | @@ -227,17 +230,17 @@ |
659 | imported as a sorted list of strings.""" |
660 | imports = {} |
661 | # Search for escaped newlines and remove them. |
662 | - searchpos = 0 |
663 | + searchpos = 0 |
664 | while True: |
665 | match = escaped_nl_regex.search(import_section, searchpos) |
666 | if match is None: |
667 | break |
668 | start = match.start() |
669 | end = match.end() |
670 | - import_section = import_section[:start]+import_section[end:] |
671 | + import_section = import_section[:start] + import_section[end:] |
672 | searchpos = start |
673 | # Search for simple one-line import statements. |
674 | - searchpos = 0 |
675 | + searchpos = 0 |
676 | while True: |
677 | match = import_regex.search(import_section, searchpos) |
678 | if match is None: |
679 | @@ -299,7 +302,7 @@ |
680 | standard_section[module] = statement |
681 | else: |
682 | thirdparty_section[module] = statement |
683 | - |
684 | + |
685 | all_import_lines = [] |
686 | # Sort within each section and generate statement strings. |
687 | sections = ( |
688 | @@ -321,7 +324,7 @@ |
689 | if len(import_lines) > 0: |
690 | all_import_lines.append('\n'.join(import_lines)) |
691 | # Sections are separated by two blank lines. |
692 | - return '\n\n'.join(all_import_lines) |
693 | + return '\n\n'.join(all_import_lines) |
694 | |
695 | |
696 | def reformat_importsection(filename): |
697 | @@ -334,17 +337,17 @@ |
698 | imports_section = pyfile[import_start:import_end] |
699 | imports = parse_import_statements(imports_section) |
700 | |
701 | - if pyfile[import_end:import_end+1] != '#': |
702 | + if pyfile[import_end:import_end + 1] != '#': |
703 | # Two newlines before anything but comments. |
704 | number_of_newlines = 3 |
705 | else: |
706 | number_of_newlines = 2 |
707 | |
708 | - new_imports = format_imports(imports)+"\n"*number_of_newlines |
709 | + new_imports = format_imports(imports) + ("\n" * number_of_newlines) |
710 | if new_imports == imports_section: |
711 | - # No change, no need to write a new file. |
712 | - return False |
713 | - |
714 | + # No change, no need to write a new file. |
715 | + return False |
716 | + |
717 | new_file = open(filename, "w") |
718 | new_file.write(pyfile[:import_start]) |
719 | new_file.write(new_imports) |
720 | @@ -372,7 +375,7 @@ |
721 | if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"): |
722 | sys.stderr.write(dedent("""\ |
723 | usage: format-imports <file or directory> ... |
724 | - |
725 | + |
726 | Type "format-imports --docstring | less" to see the documentation. |
727 | """)) |
728 | sys.exit(1) |
I also fixed some lint in utilities/ format- imports.