Merge lp:~allenap/launchpad/sub-filters-via-api-bug-672619 into lp:launchpad

Proposed by Gavin Panella
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
Reviewer Review Type Date Requested Status
Deryck Hodge (community) code Approve
Review via email: mp+44331@code.launchpad.net

Commit message

[r=deryck][ui=none][bug=672619] Expose bug subscription filters via the web service API.

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/bugs/browser/configure.zcml

- Providing navigation for bug subscription filters:

  lib/lp/registry/browser/configure.zcml
  lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
    for TestBugSubscriptionFilterNavigation
  lib/lp/registry/browser/structuralsubscription.py
    for StructuralSubscriptionNavigation

- Exporting IStructuralSubscription.bug_filters and .newBugFilter().

  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/lp/registry/browser/tests/test_structuralsubscription.py
  lib/lp/registry/interfaces/structuralsubscription.py

- Exporting IBugSubscriptionFilter.

  lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
    for TestBugSubscriptionFilterAPI
    for TestBugSubscriptionFilterAPIModifications
  lib/lp/bugs/interfaces/bugsubscriptionfilter.py
  lib/lp/bugs/interfaces/webservice.py

- Fix a weird issue where navigation was attempted on a non-flushed
  object. Haven't investigated this, but a flush prevents issues.

  lib/lp/registry/model/structuralsubscription.py

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

I also fixed some lint in utilities/format-imports.

Revision history for this message
Deryck Hodge (deryck) wrote :

Looks fine to me, per our discussions on IRC.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)