Merge lp:~wgrant/launchpad/export-structural-subscriptions-bug-392515 into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~wgrant/launchpad/export-structural-subscriptions-bug-392515
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~wgrant/launchpad/export-structural-subscriptions-bug-392515
Reviewer Review Type Date Requested Status
Gavin Panella (community) code Approve
Review via email: mp+11428@code.launchpad.net

Commit message

[r=noideayet][ui=none] (wgrant) Export structural subscriptions in the webservice API.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

= Summary =
Structural subscriptions want to be exported through the API. This is the last of four branches: the actual interface decorations.

== Proposed fix ==
Decorate IStructuralSubscription and IStructuralSubscriptionTarget to export all relevant methods and attributes.

== Implementation details ==
The export of ISST.getSubscriptions is deliberately somewhat limited. Only one level of bug subscription is used at the moment, and I hope the level setup will change, so I thought it best not to expose those details.

Apart from that, there should be nothing unobvious here.

== Tests ==
I've added lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt

 $ bin/test -vvt xx-structuralsubscription.txt

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (13.3 KiB)

Hi William,

This is a very nice patch. I have a few questions/comments, but they're small.

Gavin.

> === modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
> --- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-28 07:34:44 +0000
> +++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-09-09 13:04:34 +0000
> @@ -22,6 +22,8 @@
> patch_collection_return_type, patch_plain_parameter_type,
> patch_choice_parameter_type, patch_reference_property)
>
> +from canonical.launchpad.interfaces.structuralsubscription import (
> + IStructuralSubscription, IStructuralSubscriptionTarget)
> from lp.bugs.interfaces.bug import IBug
> from lp.bugs.interfaces.bugbranch import IBugBranch
> from lp.bugs.interfaces.bugnomination import IBugNomination
> @@ -278,3 +280,11 @@
> IPackageUpload['pocket'].vocabulary = PackagePublishingPocket
> patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)
> patch_reference_property(IPackageUpload, 'archive', IArchive)
> +
> +# IStructuralSubscription
> +patch_reference_property(
> + IStructuralSubscription, 'target', IStructuralSubscriptionTarget)
> +
> +patch_reference_property(
> + IStructuralSubscriptionTarget, 'parent_subscription_target',
> + IStructuralSubscriptionTarget)
>
> === modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py'
> --- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-08-25 11:08:42 +0000
> +++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-09-09 13:04:34 +0000
> @@ -24,6 +24,14 @@
> from canonical.launchpad import _
> from canonical.launchpad.fields import (
> ParticipatingPersonChoice, PublicPersonChoice)
> +from lp.registry.interfaces.person import IPerson
> +
> +from lazr.restful.declarations import (
> + REQUEST_USER, call_with, exported, export_as_webservice_entry,
> + export_factory_operation, export_read_operation, export_write_operation,
> + operation_parameters, operation_returns_collection_of,
> + operation_returns_entry, webservice_error)
> +from lazr.restful.fields import Reference
>
>
> class BugNotificationLevel(DBEnumeratedType):
> @@ -84,6 +92,7 @@
>
> class IStructuralSubscription(Interface):
> """A subscription to a Launchpad structure."""
> + export_as_webservice_entry()
>
> id = Int(title=_('ID'), readonly=True, required=True)
> product = Int(title=_('Product'), required=False, readonly=True)
> @@ -96,13 +105,13 @@
> title=_('Distribution series'), required=False, readonly=True)
> sourcepackagename = Int(
> title=_('Source package name'), required=False, readonly=True)
> - subscriber = ParticipatingPersonChoice(
> + subscriber = exported(ParticipatingPersonChoice(
> title=_('Subscriber'), required=True, vocabulary='ValidPersonOrTeam',
> - readonly=True, description=_("The person subscribed."))
> - subscribed_by = PublicPersonChoice(
> + readonly=True, description=_("The person subscribed.")))
> + subscribed_by = exported(PublicPersonChoice(
> title=_('Subscribed by'), required=True,
> vocabulary='ValidP...

review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

Hi Gavin,

On Wed, 2009-09-09 at 13:35 +0000, Gavin Panella wrote:
> Review: Approve code
> Hi William,
>
> This is a very nice patch. I have a few questions/comments, but they're small.

Thanks for the review.

As discussed on IRC, I've adjusted the parameter descriptions to
indicate the behaviour of omitted arguments.

At intellectronica's suggestion, I've also unexported
ISST.parent_subscription_target for now (it is considered an
implementation detail, and we can do something much nicer later).

Diff from 9230 to 9233 attached.

1=== modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py'
2--- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-08-25 11:26:58 +0000
3+++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-09-09 14:25:50 +0000
4@@ -160,10 +160,8 @@
5 :return: A sequence of `IStructuralSubscription`.
6 """
7
8- parent_subscription_target = exported(Reference(
9- schema=Interface, # IStructuralSubscriptionTarget
10- required=True, readonly=True,
11- title=_("The target's parent, or None if one doesn't exist.")))
12+ parent_subscription_target = Attribute(
13+ "The target's parent, or None if one doesn't exist.")
14
15 def addSubscription(subscriber, subscribed_by):
16 """Add a subscription for this structure.
17@@ -171,14 +169,19 @@
18 This method is used to create a new `IStructuralSubscription`
19 for the target, with no levels set.
20
21- :subscriber: The IPerson who will be subscribed.
22+ :subscriber: The IPerson who will be subscribed. If omitted,
23+ subscribed_by will be used.
24 :subscribed_by: The IPerson creating the subscription.
25 :return: The new subscription.
26 """
27
28 @operation_parameters(
29 subscriber=Reference(
30- schema=IPerson, title=_('Person'), required=False))
31+ schema=IPerson,
32+ title=_(
33+ 'Person to subscribe. If omitted, the requesting user will be'
34+ ' subscribed.'),
35+ required=False))
36 @call_with(subscribed_by=REQUEST_USER)
37 @export_factory_operation(IStructuralSubscription, [])
38 def addBugSubscription(subscriber, subscribed_by):
39@@ -188,14 +191,19 @@
40 for the target with the bug notification level set to
41 COMMENTS, the only level currently in use.
42
43- :subscriber: The IPerson who will be subscribed.
44+ :subscriber: The IPerson who will be subscribed. If omitted,
45+ subscribed_by will be used.
46 :subscribed_by: The IPerson creating the subscription.
47 :return: The new bug subscription.
48 """
49
50 @operation_parameters(
51 subscriber=Reference(
52- schema=IPerson, title=_('Person'), required=False))
53+ schema=IPerson,
54+ title=_(
55+ 'Person to unsubscribe. If omitted, the requesting user will '
56+ 'be unsubscribed.'),
57+ required=False))
58 @call_with(unsubscribed_by=REQUEST_USER)
59 @export_write_operation()
60 def removeBugSubscription(subscriber, unsubscribed_by):
61@@ -205,7 +213,8 @@
62 set the subscription's `bug_notification_level` to
63 `NOTHING`, otherwise, destroy the subscription.
64
65- :subscriber: The IPerson who will be subscribed.
66+ :subscriber: The IPerson who will be unsubscribed. If omitted,
67+ unsubscribed_by will be used.
68 :unsubscribed_by: The IPerson removing the subscription.
69 """
70

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 2009-08-28 07:34:44 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-09-09 11:28:03 +0000
4@@ -22,6 +22,8 @@
5 patch_collection_return_type, patch_plain_parameter_type,
6 patch_choice_parameter_type, patch_reference_property)
7
8+from canonical.launchpad.interfaces.structuralsubscription import (
9+ IStructuralSubscription, IStructuralSubscriptionTarget)
10 from lp.bugs.interfaces.bug import IBug
11 from lp.bugs.interfaces.bugbranch import IBugBranch
12 from lp.bugs.interfaces.bugnomination import IBugNomination
13@@ -278,3 +280,11 @@
14 IPackageUpload['pocket'].vocabulary = PackagePublishingPocket
15 patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)
16 patch_reference_property(IPackageUpload, 'archive', IArchive)
17+
18+# IStructuralSubscription
19+patch_reference_property(
20+ IStructuralSubscription, 'target', IStructuralSubscriptionTarget)
21+
22+patch_reference_property(
23+ IStructuralSubscriptionTarget, 'parent_subscription_target',
24+ IStructuralSubscriptionTarget)
25
26=== modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py'
27--- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-08-25 11:08:42 +0000
28+++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-08-25 11:26:58 +0000
29@@ -24,6 +24,14 @@
30 from canonical.launchpad import _
31 from canonical.launchpad.fields import (
32 ParticipatingPersonChoice, PublicPersonChoice)
33+from lp.registry.interfaces.person import IPerson
34+
35+from lazr.restful.declarations import (
36+ REQUEST_USER, call_with, exported, export_as_webservice_entry,
37+ export_factory_operation, export_read_operation, export_write_operation,
38+ operation_parameters, operation_returns_collection_of,
39+ operation_returns_entry, webservice_error)
40+from lazr.restful.fields import Reference
41
42
43 class BugNotificationLevel(DBEnumeratedType):
44@@ -84,6 +92,7 @@
45
46 class IStructuralSubscription(Interface):
47 """A subscription to a Launchpad structure."""
48+ export_as_webservice_entry()
49
50 id = Int(title=_('ID'), readonly=True, required=True)
51 product = Int(title=_('Product'), required=False, readonly=True)
52@@ -96,13 +105,13 @@
53 title=_('Distribution series'), required=False, readonly=True)
54 sourcepackagename = Int(
55 title=_('Source package name'), required=False, readonly=True)
56- subscriber = ParticipatingPersonChoice(
57+ subscriber = exported(ParticipatingPersonChoice(
58 title=_('Subscriber'), required=True, vocabulary='ValidPersonOrTeam',
59- readonly=True, description=_("The person subscribed."))
60- subscribed_by = PublicPersonChoice(
61+ readonly=True, description=_("The person subscribed.")))
62+ subscribed_by = exported(PublicPersonChoice(
63 title=_('Subscribed by'), required=True,
64 vocabulary='ValidPersonOrTeam', readonly=True,
65- description=_("The person creating the subscription."))
66+ description=_("The person creating the subscription.")))
67 bug_notification_level = Choice(
68 title=_("Bug notification level"), required=True,
69 vocabulary=BugNotificationLevel,
70@@ -115,19 +124,30 @@
71 default=BlueprintNotificationLevel.NOTHING,
72 description=_("The volume and type of blueprint notifications "
73 "this subscription will generate."))
74- date_created = Datetime(
75+ date_created = exported(Datetime(
76 title=_("The date on which this subscription was created."),
77- required=False)
78- date_last_updated = Datetime(
79+ required=False, readonly=True))
80+ date_last_updated = exported(Datetime(
81 title=_("The date on which this subscription was last updated."),
82- required=False)
83+ required=False, readonly=True))
84
85- target = Attribute("The structure to which this subscription belongs.")
86+ target = exported(Reference(
87+ schema=Interface, # IStructuralSubscriptionTarget
88+ required=True, readonly=True,
89+ title=_("The structure to which this subscription belongs.")))
90
91
92 class IStructuralSubscriptionTarget(Interface):
93 """A Launchpad Structure allowing users to subscribe to it."""
94+ export_as_webservice_entry()
95
96+ # We don't really want to expose the level details yet. Only
97+ # BugNotificationLevel.COMMENTS is used at this time.
98+ @call_with(
99+ min_bug_notification_level=BugNotificationLevel.COMMENTS,
100+ min_blueprint_notification_level=BlueprintNotificationLevel.NOTHING)
101+ @operation_returns_collection_of(IStructuralSubscription)
102+ @export_read_operation()
103 def getSubscriptions(min_bug_notification_level,
104 min_blueprint_notification_level):
105 """Return all the subscriptions with the specified levels.
106@@ -140,8 +160,10 @@
107 :return: A sequence of `IStructuralSubscription`.
108 """
109
110- parent_subscription_target = Attribute(
111- "The target's parent, or None if one doesn't exist.")
112+ parent_subscription_target = exported(Reference(
113+ schema=Interface, # IStructuralSubscriptionTarget
114+ required=True, readonly=True,
115+ title=_("The target's parent, or None if one doesn't exist.")))
116
117 def addSubscription(subscriber, subscribed_by):
118 """Add a subscription for this structure.
119@@ -154,6 +176,11 @@
120 :return: The new subscription.
121 """
122
123+ @operation_parameters(
124+ subscriber=Reference(
125+ schema=IPerson, title=_('Person'), required=False))
126+ @call_with(subscribed_by=REQUEST_USER)
127+ @export_factory_operation(IStructuralSubscription, [])
128 def addBugSubscription(subscriber, subscribed_by):
129 """Add a bug subscription for this structure.
130
131@@ -166,6 +193,11 @@
132 :return: The new bug subscription.
133 """
134
135+ @operation_parameters(
136+ subscriber=Reference(
137+ schema=IPerson, title=_('Person'), required=False))
138+ @call_with(unsubscribed_by=REQUEST_USER)
139+ @export_write_operation()
140 def removeBugSubscription(subscriber, unsubscribed_by):
141 """Remove a subscription to bugs from this structure.
142
143@@ -177,6 +209,9 @@
144 :unsubscribed_by: The IPerson removing the subscription.
145 """
146
147+ @operation_parameters(person=Reference(schema=IPerson))
148+ @operation_returns_entry(IStructuralSubscription)
149+ @export_read_operation()
150 def getSubscription(person):
151 """Return the subscription for `person`, if it exists."""
152
153@@ -209,7 +244,9 @@
154
155 Raised when an error occurred trying to delete a
156 structural subscription."""
157+ webservice_error(400)
158
159
160 class UserCannotSubscribePerson(Exception):
161 """User does not have permission to subscribe the person or team."""
162+ webservice_error(401)
163
164=== added file 'lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt'
165--- lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 1970-01-01 00:00:00 +0000
166+++ lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 2009-08-27 04:16:41 +0000
167@@ -0,0 +1,151 @@
168+= Structural Subscriptions =
169+
170+Structural subscriptions can be obtained from any target: a project,
171+project series, project group, distribution, distribution series or
172+distribution source package.
173+
174+ >>> login('admin@canonical.com')
175+ >>> eric_db = factory.makePerson(name='eric')
176+ >>> michael_db = factory.makePerson(name='michael')
177+ >>> pythons_db = factory.makeTeam(name='pythons', owner=michael_db)
178+ >>> pythons_db.addMember(eric_db, michael_db)
179+
180+ >>> fooix_db = factory.makeProduct(name='fooix', owner=eric_db)
181+ >>> fooix01_db = fooix_db.newSeries(eric_db, '0.1', 'Series 0.1')
182+ >>> logout()
183+
184+We can list the structural subscriptions on a target using the
185+getSubscriptions named operation. There are none just yet.
186+
187+ >>> from lazr.restful.testing.webservice import (
188+ ... pprint_collection, pprint_entry)
189+ >>> subscriptions = webservice.named_get(
190+ ... '/fooix', 'getSubscriptions').jsonBody()
191+ >>> pprint_collection(subscriptions)
192+ start: None
193+ total_size: 0
194+ ---
195+
196+Now Eric subscribes to Fooix's bug notifications.
197+
198+ >>> from canonical.launchpad.testing.pages import webservice_for_person
199+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
200+ >>> eric_webservice = webservice_for_person(
201+ ... eric_db, permission=OAuthPermission.WRITE_PRIVATE)
202+
203+ >>> print eric_webservice.named_post(
204+ ... '/fooix', 'addBugSubscription')
205+ HTTP/1.1 201 Created
206+ ...
207+ Location: http://.../fooix/+subscription/eric
208+ ...
209+
210+ >>> subscriptions = webservice.named_get(
211+ ... '/fooix', 'getSubscriptions').jsonBody()
212+ >>> pprint_collection(subscriptions)
213+ start: 0
214+ total_size: 1
215+ ---
216+ date_created: u'...'
217+ date_last_updated: u'...'
218+ resource_type_link: u'http://.../#structural_subscription'
219+ self_link: u'http://.../fooix/+subscription/eric'
220+ subscribed_by_link: u'http://.../~eric'
221+ subscriber_link: u'http://.../~eric'
222+ target_link: u'http://.../fooix'
223+ ---
224+
225+He can examine his subscription directly.
226+
227+ >>> pprint_entry(eric_webservice.named_get(
228+ ... '/fooix', 'getSubscription',
229+ ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
230+ date_created: u'...'
231+ date_last_updated: u'...'
232+ resource_type_link: u'http://.../#structural_subscription'
233+ self_link: u'http://.../fooix/+subscription/eric'
234+ subscribed_by_link: u'http://.../~eric'
235+ subscriber_link: u'http://.../~eric'
236+ target_link: u'http://.../fooix'
237+
238+If the subscription doesn't exist, None will be returned.
239+
240+ >>> print webservice.named_get(
241+ ... '/fooix', 'getSubscription',
242+ ... person=webservice.getAbsoluteUrl('/~michael')).jsonBody()
243+ None
244+
245+Eric can remove his subscription through the webservice.
246+
247+ >>> print eric_webservice.named_post(
248+ ... '/fooix', 'removeBugSubscription')
249+ HTTP/1.1 200 Ok...
250+
251+ >>> print webservice.named_get(
252+ ... '/fooix', 'getSubscription',
253+ ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()
254+ None
255+
256+Teams can be subscribed by passing in the team as an argument. Eric
257+tries this.
258+
259+ >>> print eric_webservice.named_post(
260+ ... '/fooix', 'addBugSubscription',
261+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
262+ HTTP/1.1 401 Unauthorized
263+ ...
264+ UserCannotSubscribePerson: eric does not have permission to subscribe pythons.
265+ <BLANKLINE>
266+
267+Oops, Eric isn't a team admin. Eric gets Michael to try, since he is an
268+admin by virtue of his ownership.
269+
270+ >>> michael_webservice = webservice_for_person(
271+ ... michael_db, permission=OAuthPermission.WRITE_PRIVATE)
272+
273+ >>> print michael_webservice.named_post(
274+ ... '/fooix', 'addBugSubscription',
275+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
276+ HTTP/1.1 201 Created
277+ ...
278+ Location: http://.../fooix/+subscription/pythons
279+ ...
280+
281+ >>> subscriptions = webservice.named_get(
282+ ... '/fooix', 'getSubscriptions').jsonBody()
283+ >>> pprint_collection(subscriptions)
284+ start: 0
285+ total_size: 1
286+ ---
287+ date_created: u'...'
288+ date_last_updated: u'...'
289+ resource_type_link: u'http://.../#structural_subscription'
290+ self_link: u'http://.../fooix/+subscription/pythons'
291+ subscribed_by_link: u'http://.../~michael'
292+ subscriber_link: u'http://.../~pythons'
293+ target_link: u'http://.../fooix'
294+ ---
295+
296+Eric can't unsubscribe the team either.
297+
298+ >>> print eric_webservice.named_post(
299+ ... '/fooix', 'removeBugSubscription',
300+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
301+ HTTP/1.1 401 Unauthorized
302+ ...
303+ UserCannotSubscribePerson: eric does not have permission to unsubscribe pythons.
304+ <BLANKLINE>
305+
306+Michael can, though.
307+
308+ >>> print michael_webservice.named_post(
309+ ... '/fooix', 'removeBugSubscription',
310+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
311+ HTTP/1.1 200 Ok...
312+
313+ >>> subscriptions = webservice.named_get(
314+ ... '/fooix', 'getSubscriptions').jsonBody()
315+ >>> pprint_collection(subscriptions)
316+ start: None
317+ total_size: 0
318+ ---