Merge lp:~wgrant/launchpad/export-structural-subscriptions-bug-392515 into lp:launchpad
- export-structural-subscriptions-bug-392515
- Merge into devel
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | code | Approve | |
Review via email: mp+11428@code.launchpad.net |
Commit message
[r=noideayet]
Description of the change
William Grant (wgrant) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Hi William,
This is a very nice patch. I have a few questions/comments, but they're small.
Gavin.
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -22,6 +22,8 @@
> patch_collectio
> patch_choice_
>
> +from canonical.
> + IStructuralSubs
> from lp.bugs.
> from lp.bugs.
> from lp.bugs.
> @@ -278,3 +280,11 @@
> IPackageUpload[
> patch_reference
> patch_reference
> +
> +# IStructuralSubs
> +patch_
> + IStructuralSubs
> +
> +patch_
> + IStructuralSubs
> + IStructuralSubs
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -24,6 +24,14 @@
> from canonical.launchpad import _
> from canonical.
> ParticipatingPe
> +from lp.registry.
> +
> +from lazr.restful.
> + REQUEST_USER, call_with, exported, export_
> + export_
> + operation_
> + operation_
> +from lazr.restful.fields import Reference
>
>
> class BugNotification
> @@ -84,6 +92,7 @@
>
> class IStructuralSubs
> """A subscription to a Launchpad structure."""
> + export_
>
> id = Int(title=_('ID'), readonly=True, required=True)
> product = Int(title=
> @@ -96,13 +105,13 @@
> title=_
> sourcepackagename = Int(
> title=_('Source package name'), required=False, readonly=True)
> - subscriber = ParticipatingPe
> + subscriber = exported(
> title=_
> - readonly=True, description=_("The person subscribed."))
> - subscribed_by = PublicPersonChoice(
> + readonly=True, description=_("The person subscribed.")))
> + subscribed_by = exported(
> title=_('Subscribed by'), required=True,
> vocabulary=
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_
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
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 | + --- |
= Summary =
Structural subscriptions want to be exported through the API. This is the last of four branches: the actual interface decorations.
== Proposed fix == cription and IStructuralSubs criptionTarget to export all relevant methods and attributes.
Decorate IStructuralSubs
== Implementation details == ptions 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.
The export of ISST.getSubscri
Apart from that, there should be nothing unobvious here.
== Tests == launchpad/ pagetests/ webservice/ xx-structuralsu bscription. txt
I've added lib/canonical/
$ bin/test -vvt xx-structuralsu bscription. txt