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
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-01 19:12:00 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2010-12-21 23:23:17 +0000
@@ -420,6 +420,8 @@
420# IStructuralSubscription420# IStructuralSubscription
421patch_collection_property(421patch_collection_property(
422 IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter)422 IStructuralSubscription, 'bug_filters', IBugSubscriptionFilter)
423patch_entry_return_type(
424 IStructuralSubscription, "newBugFilter", IBugSubscriptionFilter)
423patch_reference_property(425patch_reference_property(
424 IStructuralSubscription, 'target', IStructuralSubscriptionTarget)426 IStructuralSubscription, 'target', IStructuralSubscriptionTarget)
425427
426428
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2010-10-28 09:11:36 +0000
+++ lib/lp/bugs/browser/configure.zcml 2010-12-21 23:23:17 +0000
@@ -1176,4 +1176,13 @@
1176 BugWatchSetNavigation"/>1176 BugWatchSetNavigation"/>
1177 </facet>1177 </facet>
11781178
1179 <!-- Bug Subscription Filters -->
1180 <facet facet="bugs">
1181 <browser:url
1182 for="lp.bugs.interfaces.bugsubscriptionfilter.IBugSubscriptionFilter"
1183 path_expression="string:+filter/${id}"
1184 attribute_to_parent="structural_subscription"
1185 rootsite="bugs" />
1186 </facet>
1187
1179</configure>1188</configure>
11801189
=== added file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
--- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000
@@ -0,0 +1,210 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for bug subscription filter browser code."""
5
6__metaclass__ = type
7
8from functools import partial
9from urlparse import urlparse
10
11from lazr.restfulclient.errors import BadRequest
12from storm.exceptions import LostObjectError
13from testtools.matchers import StartsWith
14import transaction
15
16from canonical.launchpad.webapp.publisher import canonical_url
17from canonical.launchpad.webapp.servers import LaunchpadTestRequest
18from canonical.testing.layers import (
19 AppServerLayer,
20 LaunchpadFunctionalLayer,
21 )
22from lp.bugs.interfaces.bugtask import (
23 BugTaskImportance,
24 BugTaskStatus,
25 )
26from lp.registry.browser.structuralsubscription import (
27 StructuralSubscriptionNavigation,
28 )
29from lp.testing import (
30 person_logged_in,
31 TestCaseWithFactory,
32 ws_object,
33 )
34
35
36class TestBugSubscriptionFilterBase:
37
38 def setUp(self):
39 super(TestBugSubscriptionFilterBase, self).setUp()
40 self.owner = self.factory.makePerson(name=u"foo")
41 self.structure = self.factory.makeProduct(
42 owner=self.owner, name=u"bar")
43 with person_logged_in(self.owner):
44 self.subscription = self.structure.addBugSubscription(
45 self.owner, self.owner)
46 self.subscription_filter = self.subscription.newBugFilter()
47
48
49class TestBugSubscriptionFilterNavigation(
50 TestBugSubscriptionFilterBase, TestCaseWithFactory):
51
52 layer = LaunchpadFunctionalLayer
53
54 def test_canonical_url(self):
55 url = urlparse(canonical_url(self.subscription_filter))
56 self.assertThat(url.hostname, StartsWith("bugs."))
57 self.assertEqual(
58 "/bar/+subscription/foo/+filter/%d" % (
59 self.subscription_filter.id),
60 url.path)
61
62 def test_navigation(self):
63 request = LaunchpadTestRequest()
64 request.setTraversalStack([unicode(self.subscription_filter.id)])
65 navigation = StructuralSubscriptionNavigation(
66 self.subscription, request)
67 view = navigation.publishTraverse(request, '+filter')
68 self.assertIsNot(None, view)
69
70
71class TestBugSubscriptionFilterAPI(
72 TestBugSubscriptionFilterBase, TestCaseWithFactory):
73
74 layer = AppServerLayer
75
76 def test_visible_attributes(self):
77 # Bug subscription filters are not private objects. All attributes are
78 # visible to everyone.
79 transaction.commit()
80 # Create a service for a new person.
81 service = self.factory.makeLaunchpadService()
82 get_ws_object = partial(ws_object, service)
83 ws_subscription = get_ws_object(self.subscription)
84 ws_subscription_filter = get_ws_object(self.subscription_filter)
85 self.assertEqual(
86 ws_subscription.self_link,
87 ws_subscription_filter.structural_subscription_link)
88 self.assertEqual(
89 self.subscription_filter.find_all_tags,
90 ws_subscription_filter.find_all_tags)
91 self.assertEqual(
92 self.subscription_filter.description,
93 ws_subscription_filter.description)
94 self.assertEqual(
95 list(self.subscription_filter.statuses),
96 ws_subscription_filter.statuses)
97 self.assertEqual(
98 list(self.subscription_filter.importances),
99 ws_subscription_filter.importances)
100 self.assertEqual(
101 list(self.subscription_filter.tags),
102 ws_subscription_filter.tags)
103
104 def test_structural_subscription_cannot_be_modified(self):
105 # Bug filters cannot be moved from one structural subscription to
106 # another. In other words, the structural_subscription field is
107 # read-only.
108 user = self.factory.makePerson(name=u"baz")
109 with person_logged_in(self.owner):
110 user_subscription = self.structure.addBugSubscription(user, user)
111 transaction.commit()
112 # Create a service for the structure owner.
113 service = self.factory.makeLaunchpadService(self.owner)
114 get_ws_object = partial(ws_object, service)
115 ws_user_subscription = get_ws_object(user_subscription)
116 ws_subscription_filter = get_ws_object(self.subscription_filter)
117 ws_subscription_filter.structural_subscription = ws_user_subscription
118 error = self.assertRaises(BadRequest, ws_subscription_filter.lp_save)
119 self.assertEqual(400, error.response.status)
120 self.assertEqual(
121 self.subscription,
122 self.subscription_filter.structural_subscription)
123
124
125class TestBugSubscriptionFilterAPIModifications(
126 TestBugSubscriptionFilterBase, TestCaseWithFactory):
127
128 layer = AppServerLayer
129
130 def setUp(self):
131 super(TestBugSubscriptionFilterAPIModifications, self).setUp()
132 transaction.commit()
133 self.service = self.factory.makeLaunchpadService(self.owner)
134 self.ws_subscription_filter = ws_object(
135 self.service, self.subscription_filter)
136
137 def test_modify_tags_fields(self):
138 # Two tags-related fields - find_all_tags and tags - can be
139 # modified. The other two tags-related fields - include_any_tags and
140 # exclude_any_tags - are not exported because the tags field provides
141 # a more intuitive way to update them (from the perspective of an API
142 # consumer).
143 self.assertFalse(self.subscription_filter.find_all_tags)
144 self.assertFalse(self.subscription_filter.include_any_tags)
145 self.assertFalse(self.subscription_filter.exclude_any_tags)
146 self.assertEqual(set(), self.subscription_filter.tags)
147
148 # Modify, save, and start a new transaction.
149 self.ws_subscription_filter.find_all_tags = True
150 self.ws_subscription_filter.tags = ["foo", "-bar", "*", "-*"]
151 self.ws_subscription_filter.lp_save()
152 transaction.begin()
153
154 # Updated state.
155 self.assertTrue(self.subscription_filter.find_all_tags)
156 self.assertTrue(self.subscription_filter.include_any_tags)
157 self.assertTrue(self.subscription_filter.exclude_any_tags)
158 self.assertEqual(
159 set(["*", "-*", "foo", "-bar"]),
160 self.subscription_filter.tags)
161
162 def test_modify_description(self):
163 # The description can be modified.
164 self.assertEqual(
165 None, self.subscription_filter.description)
166
167 # Modify, save, and start a new transaction.
168 self.ws_subscription_filter.description = u"It's late."
169 self.ws_subscription_filter.lp_save()
170 transaction.begin()
171
172 # Updated state.
173 self.assertEqual(
174 u"It's late.", self.subscription_filter.description)
175
176 def test_modify_statuses(self):
177 # The statuses field can be modified.
178 self.assertEqual(set(), self.subscription_filter.statuses)
179
180 # Modify, save, and start a new transaction.
181 self.ws_subscription_filter.statuses = ["New", "Triaged"]
182 self.ws_subscription_filter.lp_save()
183 transaction.begin()
184
185 # Updated state.
186 self.assertEqual(
187 set([BugTaskStatus.NEW, BugTaskStatus.TRIAGED]),
188 self.subscription_filter.statuses)
189
190 def test_modify_importances(self):
191 # The importances field can be modified.
192 self.assertEqual(set(), self.subscription_filter.importances)
193
194 # Modify, save, and start a new transaction.
195 self.ws_subscription_filter.importances = ["Low", "High"]
196 self.ws_subscription_filter.lp_save()
197 transaction.begin()
198
199 # Updated state.
200 self.assertEqual(
201 set([BugTaskImportance.LOW, BugTaskImportance.HIGH]),
202 self.subscription_filter.importances)
203
204 def test_delete(self):
205 # Subscription filters can be deleted.
206 self.ws_subscription_filter.lp_delete()
207 transaction.begin()
208 self.assertRaises(
209 LostObjectError, getattr, self.subscription_filter,
210 "find_all_tags")
0211
=== modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py'
--- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-10-04 13:24:54 +0000
+++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2010-12-21 23:23:17 +0000
@@ -9,12 +9,18 @@
9 ]9 ]
1010
1111
12from lazr.restful.declarations import (
13 export_as_webservice_entry,
14 export_destructor_operation,
15 exported,
16 )
12from lazr.restful.fields import Reference17from lazr.restful.fields import Reference
13from zope.interface import Interface18from zope.interface import Interface
14from zope.schema import (19from zope.schema import (
15 Bool,20 Bool,
16 Choice,21 Choice,
17 FrozenSet,22 FrozenSet,
23 Int,
18 Text,24 Text,
19 )25 )
2026
@@ -32,14 +38,18 @@
32class IBugSubscriptionFilterAttributes(Interface):38class IBugSubscriptionFilterAttributes(Interface):
33 """Attributes of `IBugSubscriptionFilter`."""39 """Attributes of `IBugSubscriptionFilter`."""
3440
35 structural_subscription = Reference(41 id = Int(required=True, readonly=True)
36 IStructuralSubscription,42
37 title=_("Structural subscription"),43 structural_subscription = exported(
38 required=True, readonly=True)44 Reference(
3945 IStructuralSubscription,
40 find_all_tags = Bool(46 title=_("Structural subscription"),
41 title=_("All given tags must be found, or any."),47 required=True, readonly=True))
42 required=True, default=False)48
49 find_all_tags = exported(
50 Bool(
51 title=_("All given tags must be found, or any."),
52 required=True, default=False))
43 include_any_tags = Bool(53 include_any_tags = Bool(
44 title=_("Include any tags."),54 title=_("Include any tags."),
45 required=True, default=False)55 required=True, default=False)
@@ -47,31 +57,36 @@
47 title=_("Exclude all tags."),57 title=_("Exclude all tags."),
48 required=True, default=False)58 required=True, default=False)
4959
50 description = Text(60 description = exported(
51 title=_("Description of this filter."),61 Text(
52 required=False)62 title=_("Description of this filter."),
5363 required=False))
54 statuses = FrozenSet(64
55 title=_("The statuses to filter on."),65 statuses = exported(
56 required=True, default=frozenset(),66 FrozenSet(
57 value_type=Choice(67 title=_("The statuses to filter on."),
58 title=_('Status'), vocabulary=BugTaskStatus))68 required=True, default=frozenset(),
5969 value_type=Choice(
60 importances = FrozenSet(70 title=_('Status'), vocabulary=BugTaskStatus)))
61 title=_("The importances to filter on."),71
62 required=True, default=frozenset(),72 importances = exported(
63 value_type=Choice(73 FrozenSet(
64 title=_('Importance'), vocabulary=BugTaskImportance))74 title=_("The importances to filter on."),
6575 required=True, default=frozenset(),
66 tags = FrozenSet(76 value_type=Choice(
67 title=_("The tags to filter on."),77 title=_('Importance'), vocabulary=BugTaskImportance)))
68 required=True, default=frozenset(),78
69 value_type=SearchTag())79 tags = exported(
80 FrozenSet(
81 title=_("The tags to filter on."),
82 required=True, default=frozenset(),
83 value_type=SearchTag()))
7084
7185
72class IBugSubscriptionFilterMethods(Interface):86class IBugSubscriptionFilterMethods(Interface):
73 """Methods of `IBugSubscriptionFilter`."""87 """Methods of `IBugSubscriptionFilter`."""
7488
89 @export_destructor_operation()
75 def delete():90 def delete():
76 """Delete this bug subscription filter."""91 """Delete this bug subscription filter."""
7792
@@ -79,3 +94,4 @@
79class IBugSubscriptionFilter(94class IBugSubscriptionFilter(
80 IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods):95 IBugSubscriptionFilterAttributes, IBugSubscriptionFilterMethods):
81 """A bug subscription filter."""96 """A bug subscription filter."""
97 export_as_webservice_entry()
8298
=== modified file 'lib/lp/bugs/interfaces/webservice.py'
--- lib/lp/bugs/interfaces/webservice.py 2010-12-01 22:18:07 +0000
+++ lib/lp/bugs/interfaces/webservice.py 2010-12-21 23:23:17 +0000
@@ -50,7 +50,6 @@
50from lp.bugs.interfaces.bugattachment import IBugAttachment50from lp.bugs.interfaces.bugattachment import IBugAttachment
51from lp.bugs.interfaces.bugbranch import IBugBranch51from lp.bugs.interfaces.bugbranch import IBugBranch
52from lp.bugs.interfaces.buglink import IBugLinkTarget52from lp.bugs.interfaces.buglink import IBugLinkTarget
53from lp.bugs.interfaces.malone import IMaloneApplication
54from lp.bugs.interfaces.bugnomination import (53from lp.bugs.interfaces.bugnomination import (
55 BugNominationStatusError,54 BugNominationStatusError,
56 IBugNomination,55 IBugNomination,
@@ -58,6 +57,7 @@
58 NominationSeriesObsoleteError,57 NominationSeriesObsoleteError,
59 )58 )
60from lp.bugs.interfaces.bugsubscription import IBugSubscription59from lp.bugs.interfaces.bugsubscription import IBugSubscription
60from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
61from lp.bugs.interfaces.bugtarget import (61from lp.bugs.interfaces.bugtarget import (
62 IBugTarget,62 IBugTarget,
63 IHasBugs,63 IHasBugs,
@@ -82,6 +82,11 @@
82 ICve,82 ICve,
83 ICveSet,83 ICveSet,
84 )84 )
85from lp.bugs.interfaces.malone import IMaloneApplication
86
87
88
8589
90
86# XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular91# XXX: JonathanLange 2010-11-09 bug=673083: Legacy work-around for circular
87# import bugs. Break this up into a per-package thing.92# import bugs. Break this up into a per-package thing.
88from canonical.launchpad.interfaces import _schema_circular_imports93from canonical.launchpad.interfaces import _schema_circular_imports
8994
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-12-15 22:05:43 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-12-21 23:23:17 +0000
@@ -2225,6 +2225,9 @@
2225 for="lp.registry.interfaces.structuralsubscription.IStructuralSubscription"2225 for="lp.registry.interfaces.structuralsubscription.IStructuralSubscription"
2226 path_expression="string:+subscription/${subscriber/name}"2226 path_expression="string:+subscription/${subscriber/name}"
2227 attribute_to_parent="target"/>2227 attribute_to_parent="target"/>
2228 <browser:navigation
2229 module="lp.registry.browser.structuralsubscription"
2230 classes="StructuralSubscriptionNavigation"/>
22282231
2229 <browser:url2232 <browser:url
2230 for="lp.registry.interfaces.personproduct.IPersonProduct"2233 for="lp.registry.interfaces.personproduct.IPersonProduct"
22312234
=== modified file 'lib/lp/registry/browser/structuralsubscription.py'
--- lib/lp/registry/browser/structuralsubscription.py 2010-11-29 12:25:43 +0000
+++ lib/lp/registry/browser/structuralsubscription.py 2010-12-21 23:23:17 +0000
@@ -23,13 +23,14 @@
23 SimpleVocabulary,23 SimpleVocabulary,
24 )24 )
2525
26from canonical.launchpad.webapp import (26from canonical.launchpad.webapp.authorization import check_permission
27from canonical.launchpad.webapp.menu import Link
28from canonical.launchpad.webapp.publisher import (
27 canonical_url,29 canonical_url,
28 LaunchpadView,30 LaunchpadView,
31 Navigation,
29 stepthrough,32 stepthrough,
30 )33 )
31from canonical.launchpad.webapp.authorization import check_permission
32from canonical.launchpad.webapp.menu import Link
33from canonical.widgets import LabeledMultiCheckBoxWidget34from canonical.widgets import LabeledMultiCheckBoxWidget
34from lp.app.browser.launchpadform import (35from lp.app.browser.launchpadform import (
35 action,36 action,
@@ -44,14 +45,29 @@
44from lp.registry.interfaces.milestone import IProjectGroupMilestone45from lp.registry.interfaces.milestone import IProjectGroupMilestone
45from lp.registry.interfaces.person import IPersonSet46from lp.registry.interfaces.person import IPersonSet
46from lp.registry.interfaces.structuralsubscription import (47from lp.registry.interfaces.structuralsubscription import (
48 IStructuralSubscription,
47 IStructuralSubscriptionForm,49 IStructuralSubscriptionForm,
48 IStructuralSubscriptionTarget,50 IStructuralSubscriptionTarget,
49 )51 )
50from lp.services.propertycache import cachedproperty52from lp.services.propertycache import cachedproperty
5153
5254
55class StructuralSubscriptionNavigation(Navigation):
56
57 usedfor = IStructuralSubscription
58
59 @stepthrough("+filter")
60 def bug_filter(self, filter_id):
61 bug_filter_id = int(filter_id)
62 for bug_filter in self.context.bug_filters:
63 if bug_filter.id == bug_filter_id:
64 return bug_filter
65 return None
66
67
53class StructuralSubscriptionView(LaunchpadFormView,68class StructuralSubscriptionView(LaunchpadFormView,
54 AdvancedSubscriptionMixin):69 AdvancedSubscriptionMixin):
70
55 """View class for structural subscriptions."""71 """View class for structural subscriptions."""
5672
57 schema = IStructuralSubscriptionForm73 schema = IStructuralSubscriptionForm
5874
=== modified file 'lib/lp/registry/browser/tests/test_structuralsubscription.py'
--- lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-11-30 11:38:12 +0000
+++ lib/lp/registry/browser/tests/test_structuralsubscription.py 2010-12-21 23:23:17 +0000
@@ -3,7 +3,10 @@
33
4"""Tests for structural subscription traversal."""4"""Tests for structural subscription traversal."""
55
6from urlparse import urlparse
7
6from lazr.restful.testing.webservice import FakeRequest8from lazr.restful.testing.webservice import FakeRequest
9import transaction
7from zope.publisher.interfaces import NotFound10from zope.publisher.interfaces import NotFound
811
9from canonical.launchpad.ftests import (12from canonical.launchpad.ftests import (
@@ -14,6 +17,7 @@
14from canonical.launchpad.webapp.publisher import canonical_url17from canonical.launchpad.webapp.publisher import canonical_url
15from canonical.launchpad.webapp.servers import StepsToGo18from canonical.launchpad.webapp.servers import StepsToGo
16from canonical.testing.layers import (19from canonical.testing.layers import (
20 AppServerLayer,
17 DatabaseFunctionalLayer,21 DatabaseFunctionalLayer,
18 LaunchpadFunctionalLayer,22 LaunchpadFunctionalLayer,
19 )23 )
@@ -27,13 +31,15 @@
27from lp.registry.browser.productseries import ProductSeriesNavigation31from lp.registry.browser.productseries import ProductSeriesNavigation
28from lp.registry.browser.project import ProjectNavigation32from lp.registry.browser.project import ProjectNavigation
29from lp.registry.browser.structuralsubscription import (33from lp.registry.browser.structuralsubscription import (
30 StructuralSubscriptionView)34 StructuralSubscriptionView,
35 )
31from lp.registry.enum import BugNotificationLevel36from lp.registry.enum import BugNotificationLevel
32from lp.testing import (37from lp.testing import (
33 feature_flags,38 feature_flags,
34 person_logged_in,39 person_logged_in,
35 set_feature_flag,40 set_feature_flag,
36 TestCaseWithFactory,41 TestCaseWithFactory,
42 ws_object,
37 )43 )
38from lp.testing.views import create_initialized_view44from lp.testing.views import create_initialized_view
3945
@@ -356,3 +362,49 @@
356 self.assertEqual(362 self.assertEqual(
357 "To all bugs in %s" % self.target.displayname,363 "To all bugs in %s" % self.target.displayname,
358 self.view.target_label)364 self.view.target_label)
365
366
367class TestStructuralSubscriptionAPI(TestCaseWithFactory):
368
369 layer = AppServerLayer
370
371 def setUp(self):
372 super(TestStructuralSubscriptionAPI, self).setUp()
373 self.owner = self.factory.makePerson(name=u"foo")
374 self.structure = self.factory.makeProduct(
375 owner=self.owner, name=u"bar")
376 with person_logged_in(self.owner):
377 self.subscription = self.structure.addBugSubscription(
378 self.owner, self.owner)
379 transaction.commit()
380 self.service = self.factory.makeLaunchpadService(self.owner)
381 self.ws_subscription = ws_object(self.service, self.subscription)
382
383 def test_newBugFilter(self):
384 # New bug subscription filters can be created with newBugFilter().
385 ws_subscription_filter = self.ws_subscription.newBugFilter()
386 self.assertEqual(
387 "bug_subscription_filter",
388 urlparse(ws_subscription_filter.resource_type_link).fragment)
389 self.assertEqual(
390 ws_subscription_filter.structural_subscription.self_link,
391 self.ws_subscription.self_link)
392
393 def test_bug_filters(self):
394 # The bug_filters property is a collection of IBugSubscriptionFilter
395 # instances previously created by newBugFilter().
396 bug_filter_links = lambda: set(
397 bug_filter.self_link for bug_filter in (
398 self.ws_subscription.bug_filters))
399 self.assertEqual(set(), bug_filter_links())
400 # A new filter appears in the bug_filters collection.
401 ws_subscription_filter1 = self.ws_subscription.newBugFilter()
402 self.assertEqual(
403 set([ws_subscription_filter1.self_link]),
404 bug_filter_links())
405 # A second new filter also appears in the bug_filters collection.
406 ws_subscription_filter2 = self.ws_subscription.newBugFilter()
407 self.assertEqual(
408 set([ws_subscription_filter1.self_link,
409 ws_subscription_filter2.self_link]),
410 bug_filter_links())
359411
=== modified file 'lib/lp/registry/interfaces/structuralsubscription.py'
--- lib/lp/registry/interfaces/structuralsubscription.py 2010-11-09 11:00:55 +0000
+++ lib/lp/registry/interfaces/structuralsubscription.py 2010-12-21 23:23:17 +0000
@@ -129,11 +129,12 @@
129 required=True, readonly=True,129 required=True, readonly=True,
130 title=_("The structure to which this subscription belongs.")))130 title=_("The structure to which this subscription belongs.")))
131131
132 bug_filters = CollectionField(132 bug_filters = exported(CollectionField(
133 title=_('List of bug filters that narrow this subscription.'),133 title=_('List of bug filters that narrow this subscription.'),
134 readonly=True, required=False,134 readonly=True, required=False,
135 value_type=Reference(schema=Interface))135 value_type=Reference(schema=Interface)))
136136
137 @export_factory_operation(Interface, [])
137 def newBugFilter():138 def newBugFilter():
138 """Returns a new `BugSubscriptionFilter` for this subscription."""139 """Returns a new `BugSubscriptionFilter` for this subscription."""
139140
140141
=== modified file 'lib/lp/registry/model/structuralsubscription.py'
--- lib/lp/registry/model/structuralsubscription.py 2010-11-26 17:08:03 +0000
+++ lib/lp/registry/model/structuralsubscription.py 2010-12-21 23:23:17 +0000
@@ -160,6 +160,8 @@
160 """See `IStructuralSubscription`."""160 """See `IStructuralSubscription`."""
161 bug_filter = BugSubscriptionFilter()161 bug_filter = BugSubscriptionFilter()
162 bug_filter.structural_subscription = self162 bug_filter.structural_subscription = self
163 # This flush is needed for the web service API.
164 IStore(StructuralSubscription).flush()
163 return bug_filter165 return bug_filter
164166
165167
166168
=== modified file 'lib/lp/registry/stories/webservice/xx-structuralsubscription.txt'
--- lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-08-05 20:49:08 +0000
+++ lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-12-21 23:23:17 +0000
@@ -46,6 +46,7 @@
46 start: 046 start: 0
47 total_size: 147 total_size: 1
48 ---48 ---
49 bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters'
49 date_created: u'...'50 date_created: u'...'
50 date_last_updated: u'...'51 date_last_updated: u'...'
51 resource_type_link: u'http://.../#structural_subscription'52 resource_type_link: u'http://.../#structural_subscription'
@@ -60,6 +61,7 @@
60 >>> pprint_entry(eric_webservice.named_get(61 >>> pprint_entry(eric_webservice.named_get(
61 ... '/fooix', 'getSubscription',62 ... '/fooix', 'getSubscription',
62 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())63 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
64 bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters'
63 date_created: u'...'65 date_created: u'...'
64 date_last_updated: u'...'66 date_last_updated: u'...'
65 resource_type_link: u'http://.../#structural_subscription'67 resource_type_link: u'http://.../#structural_subscription'
@@ -117,6 +119,7 @@
117 start: 0119 start: 0
118 total_size: 1120 total_size: 1
119 ---121 ---
122 bug_filters_collection_link: u'.../fooix/+subscription/pythons/bug_filters'
120 date_created: u'...'123 date_created: u'...'
121 date_last_updated: u'...'124 date_last_updated: u'...'
122 resource_type_link: u'http://.../#structural_subscription'125 resource_type_link: u'http://.../#structural_subscription'
123126
=== modified file 'utilities/format-imports'
--- utilities/format-imports 2010-08-27 20:17:23 +0000
+++ utilities/format-imports 2010-12-21 23:23:17 +0000
@@ -24,7 +24,7 @@
24that start with "import" or "from" or are indented with at least one space or24that start with "import" or "from" or are indented with at least one space or
25are blank lines. Comment lines are also included if they are followed by an25are blank lines. Comment lines are also included if they are followed by an
26import statement. An inital __future__ import and a module docstring are26import statement. An inital __future__ import and a module docstring are
27explicitly skipped. 27explicitly skipped.
2828
29The import section is rewritten as three subsections, each separated by a29The import section is rewritten as three subsections, each separated by a
30blank line. Any of the sections may be empty.30blank line. Any of the sections may be empty.
@@ -123,7 +123,7 @@
123 from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \123 from lp.app.verylongnames.orverlydeep.modulestructure.leavenoroom \
124 import object124 import object
125}}}125}}}
126""" 126"""
127127
128__metaclass__ = type128__metaclass__ = type
129129
@@ -145,7 +145,8 @@
145 "(?P<objects>[*]|[a-zA-Z0-9_, ]+)"145 "(?P<objects>[*]|[a-zA-Z0-9_, ]+)"
146 "(?P<comment>#.*)?$", re.M)146 "(?P<comment>#.*)?$", re.M)
147from_import_multi_regex = re.compile(147from_import_multi_regex = re.compile(
148 "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$", re.M)148 "^from +(?P<module>.+) +import *[(](?P<objects>[a-zA-Z0-9_, \n]+)[)]$",
149 re.M)
149comment_regex = re.compile(150comment_regex = re.compile(
150 "(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M)151 "(?P<comment>(^#.+\n)+)(^import|^from) +(?P<module>[a-zA-Z0-9_.]+)", re.M)
151split_regex = re.compile(",\s*")152split_regex = re.compile(",\s*")
@@ -153,14 +154,16 @@
153# Module docstrings are multiline (""") strings that are not indented and are154# Module docstrings are multiline (""") strings that are not indented and are
154# followed at some point by an import .155# followed at some point by an import .
155module_docstring_regex = re.compile(156module_docstring_regex = re.compile(
156 '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)', re.M | re.S)157 '(?P<docstring>^["]{3}[^"]+["]{3}\n).*^(import |from .+ import)',
158 re.M | re.S)
157# The imports section starts with an import state that is not a __future__159# The imports section starts with an import state that is not a __future__
158# import and consists of import lines, indented lines, empty lines and160# import and consists of import lines, indented lines, empty lines and
159# comments which are followed by an import line. Sometimes we even find161# comments which are followed by an import line. Sometimes we even find
160# lines that contain a single ")"... :-(162# lines that contain a single ")"... :-(
161imports_section_regex = re.compile(163imports_section_regex = re.compile(
162 "(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n"164 "(^#.+\n)*^(import|(from ((?!__future__)\S+) import)).*\n"
163 "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) .+\n)|^\n|^[)]\n)*",165 "(^import .+\n|^from .+\n|^[\t ]+.+\n|(^#.+\n)+((^import|^from) "
166 ".+\n)|^\n|^[)]\n)*",
164 re.M)167 re.M)
165168
166169
@@ -227,17 +230,17 @@
227 imported as a sorted list of strings."""230 imported as a sorted list of strings."""
228 imports = {}231 imports = {}
229 # Search for escaped newlines and remove them.232 # Search for escaped newlines and remove them.
230 searchpos = 0233 searchpos = 0
231 while True:234 while True:
232 match = escaped_nl_regex.search(import_section, searchpos)235 match = escaped_nl_regex.search(import_section, searchpos)
233 if match is None:236 if match is None:
234 break237 break
235 start = match.start()238 start = match.start()
236 end = match.end()239 end = match.end()
237 import_section = import_section[:start]+import_section[end:]240 import_section = import_section[:start] + import_section[end:]
238 searchpos = start241 searchpos = start
239 # Search for simple one-line import statements.242 # Search for simple one-line import statements.
240 searchpos = 0243 searchpos = 0
241 while True:244 while True:
242 match = import_regex.search(import_section, searchpos)245 match = import_regex.search(import_section, searchpos)
243 if match is None:246 if match is None:
@@ -299,7 +302,7 @@
299 standard_section[module] = statement302 standard_section[module] = statement
300 else:303 else:
301 thirdparty_section[module] = statement304 thirdparty_section[module] = statement
302 305
303 all_import_lines = []306 all_import_lines = []
304 # Sort within each section and generate statement strings.307 # Sort within each section and generate statement strings.
305 sections = (308 sections = (
@@ -321,7 +324,7 @@
321 if len(import_lines) > 0:324 if len(import_lines) > 0:
322 all_import_lines.append('\n'.join(import_lines))325 all_import_lines.append('\n'.join(import_lines))
323 # Sections are separated by two blank lines.326 # Sections are separated by two blank lines.
324 return '\n\n'.join(all_import_lines) 327 return '\n\n'.join(all_import_lines)
325328
326329
327def reformat_importsection(filename):330def reformat_importsection(filename):
@@ -334,17 +337,17 @@
334 imports_section = pyfile[import_start:import_end]337 imports_section = pyfile[import_start:import_end]
335 imports = parse_import_statements(imports_section)338 imports = parse_import_statements(imports_section)
336339
337 if pyfile[import_end:import_end+1] != '#':340 if pyfile[import_end:import_end + 1] != '#':
338 # Two newlines before anything but comments.341 # Two newlines before anything but comments.
339 number_of_newlines = 3342 number_of_newlines = 3
340 else:343 else:
341 number_of_newlines = 2344 number_of_newlines = 2
342345
343 new_imports = format_imports(imports)+"\n"*number_of_newlines346 new_imports = format_imports(imports) + ("\n" * number_of_newlines)
344 if new_imports == imports_section:347 if new_imports == imports_section:
345 # No change, no need to write a new file.348 # No change, no need to write a new file.
346 return False349 return False
347 350
348 new_file = open(filename, "w")351 new_file = open(filename, "w")
349 new_file.write(pyfile[:import_start])352 new_file.write(pyfile[:import_start])
350 new_file.write(new_imports)353 new_file.write(new_imports)
@@ -372,7 +375,7 @@
372 if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"):375 if len(sys.argv) == 1 or sys.argv[1] in ("-h", "-?", "--help"):
373 sys.stderr.write(dedent("""\376 sys.stderr.write(dedent("""\
374 usage: format-imports <file or directory> ...377 usage: format-imports <file or directory> ...
375 378
376 Type "format-imports --docstring | less" to see the documentation.379 Type "format-imports --docstring | less" to see the documentation.
377 """))380 """))
378 sys.exit(1)381 sys.exit(1)