Merge lp:~danilo/launchpad/merge-ss-with-filters-urls into lp:launchpad/db-devel

Proposed by Данило Шеган
Status: Work in progress
Proposed branch: lp:~danilo/launchpad/merge-ss-with-filters-urls
Merge into: lp:launchpad/db-devel
Diff against target: 393 lines (+117/-38)
9 files modified
lib/lp/bugs/browser/configure.zcml (+1/-1)
lib/lp/bugs/browser/structuralsubscription.py (+1/-4)
lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py (+2/-1)
lib/lp/bugs/browser/tests/test_structuralsubscription.py (+47/-12)
lib/lp/bugs/interfaces/structuralsubscription.py (+9/-6)
lib/lp/bugs/model/structuralsubscription.py (+11/-1)
lib/lp/bugs/tests/test_structuralsubscriptiontarget.py (+27/-0)
lib/lp/registry/stories/person/xx-person-subscriptions.txt (+2/-2)
lib/lp/registry/stories/webservice/xx-structuralsubscription.txt (+17/-11)
To merge this branch: bzr merge lp:~danilo/launchpad/merge-ss-with-filters-urls
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+47403@code.launchpad.net

Commit message

[r=gmb][ui=none][no-qa] Switch structural subscription URLs to not be based on person names by using IDs instead (planning to have more than one per person).

Description of the change

= Make StructuralSubscription URLs independent of the person =

In accordance with our higher-level goal of having multiple structural subscription per-target, per-person, we need to have a URL for structural subscriptions that is not limited to one per person.

At the moment, canonical_url for a SS is /<target>/+subscription/<person> and we are changing that to be /<target>/+subscription/<id>

== Pre-implementation notes ==

I discussed with Gary the option of putting all SSs on a top-level StructuralSubscription object, and we decided to keep it simple and isolated by keeping the <target> (one of product, productseries, distroseries, milestone, sourcepackage), while not allowing one to reach SSs that don't belong there.

== Implementation details ==

I provided a getSubscriptionByID on IStructuralSubscriptionTarget class, which is the core of the implementation. Most of the other stuff is test updates.

== Tests ==

bin/test -cvvt structuralsubscription

== Demo and Q/A ==

Check that structural subscriptions appear on appropriate links on QA staging.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/structuralsubscription.py
  lib/lp/registry/browser/tests/test_structuralsubscription.py
  lib/lp/registry/interfaces/structuralsubscription.py
  lib/lp/registry/stories/webservice/xx-structuralsubscription.txt
  lib/lp/registry/tests/test_structuralsubscriptiontarget.py
  lib/lp/registry/model/structuralsubscription.py

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Just one minor change needed, which we discussed on IRC:

[13:57] gmb:
danilo: self.structure and self.ws_structure in TestStructuralSubscriptionTargetAPI are a bit ambiguous - enough so that a reader would need to find their definition to find out what they are, anyway. I think that self.subscription_target and .ws_subscription_target would be better names. What do you think?
[13:57] danilos:
gmb, agreed!

review: Approve (code)
Revision history for this message
Gary Poster (gary) wrote :

As you had mentioned at the sprint, after looking deeper in the code, I think we will be productive faster by sticking to one subscription and multiple separate filter records. We will move bug_notification_level from structural subscription to filter. I am working on a branch to do that now.

We may return to this model (and the goal of collapsing structural subscriptions and filters) in a later increment, or maybe not. For now, let's hold off on this branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/browser/configure.zcml 2011-01-31 09:32:26 +0000
@@ -1195,7 +1195,7 @@
11951195
1196 <browser:url1196 <browser:url
1197 for="lp.bugs.interfaces.structuralsubscription.IStructuralSubscription"1197 for="lp.bugs.interfaces.structuralsubscription.IStructuralSubscription"
1198 path_expression="string:+subscription/${subscriber/name}"1198 path_expression="string:+subscription/${id}"
1199 attribute_to_parent="target"/>1199 attribute_to_parent="target"/>
1200 <browser:navigation1200 <browser:navigation
1201 module="lp.bugs.browser.structuralsubscription"1201 module="lp.bugs.browser.structuralsubscription"
12021202
=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py 2011-01-31 09:32:26 +0000
@@ -12,7 +12,6 @@
1212
13from operator import attrgetter13from operator import attrgetter
1414
15from zope.component import getUtility
16from zope.formlib import form15from zope.formlib import form
17from zope.schema import (16from zope.schema import (
18 Choice,17 Choice,
@@ -48,7 +47,6 @@
48 IDistributionSourcePackage,47 IDistributionSourcePackage,
49 )48 )
50from lp.registry.interfaces.milestone import IProjectGroupMilestone49from lp.registry.interfaces.milestone import IProjectGroupMilestone
51from lp.registry.interfaces.person import IPersonSet
52from lp.services.propertycache import cachedproperty50from lp.services.propertycache import cachedproperty
5351
5452
@@ -347,8 +345,7 @@
347 @stepthrough('+subscription')345 @stepthrough('+subscription')
348 def traverse_structuralsubscription(self, name):346 def traverse_structuralsubscription(self, name):
349 """Traverses +subscription portions of URLs."""347 """Traverses +subscription portions of URLs."""
350 person = getUtility(IPersonSet).getByName(name)348 return self.context.getSubscriptionByID(subscription_id=int(name))
351 return self.context.getSubscription(person)
352349
353350
354class StructuralSubscriptionMenuMixin:351class StructuralSubscriptionMenuMixin:
355352
=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py'
--- lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py 2011-01-31 09:32:26 +0000
@@ -61,7 +61,8 @@
61 url = urlparse(canonical_url(self.subscription_filter))61 url = urlparse(canonical_url(self.subscription_filter))
62 self.assertThat(url.hostname, StartsWith("bugs."))62 self.assertThat(url.hostname, StartsWith("bugs."))
63 self.assertEqual(63 self.assertEqual(
64 "/bar/+subscription/foo/+filter/%d" % (64 "/bar/+subscription/%d/+filter/%d" % (
65 self.subscription.id,
65 self.subscription_filter.id),66 self.subscription_filter.id),
66 url.path)67 url.path)
6768
6869
=== modified file 'lib/lp/bugs/browser/tests/test_structuralsubscription.py'
--- lib/lp/bugs/browser/tests/test_structuralsubscription.py 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/browser/tests/test_structuralsubscription.py 2011-01-31 09:32:26 +0000
@@ -61,10 +61,12 @@
61 super(StructuralSubscriptionTraversalTestBase, self).setUp()61 super(StructuralSubscriptionTraversalTestBase, self).setUp()
62 login('foo.bar@canonical.com')62 login('foo.bar@canonical.com')
63 self.eric = self.factory.makePerson(name='eric')63 self.eric = self.factory.makePerson(name='eric')
64 self.michael = self.factory.makePerson(name='michael')
6564
66 self.setUpTarget()65 self.setUpTarget()
67 self.target.addBugSubscription(self.eric, self.eric)66 self.subscription = self.target.addBugSubscription(
67 self.eric, self.eric)
68 # To make sure subscription.id is defined, we commit the transaction.
69 transaction.commit()
6870
69 def setUpTarget(self):71 def setUpTarget(self):
70 self.target = self.factory.makeProduct(name='fooix')72 self.target = self.factory.makeProduct(name='fooix')
@@ -73,7 +75,7 @@
73 def test_structural_subscription_traversal(self):75 def test_structural_subscription_traversal(self):
74 # Verify that an existing structural subscription can be76 # Verify that an existing structural subscription can be
75 # reached from the target.77 # reached from the target.
76 request = FakeLaunchpadRequest([], ['eric'])78 request = FakeLaunchpadRequest([], [str(self.subscription.id)])
77 self.assertEqual(79 self.assertEqual(
78 self.target.getSubscription(self.eric),80 self.target.getSubscription(self.eric),
79 self.navigation(self.target, request).publishTraverse(81 self.navigation(self.target, request).publishTraverse(
@@ -81,17 +83,22 @@
8183
82 def test_missing_structural_subscription_traversal(self):84 def test_missing_structural_subscription_traversal(self):
83 # Verify that a NotFound is raised when attempting to reach85 # Verify that a NotFound is raised when attempting to reach
84 # a structural subscription for an person without one.86 # a structural subscription which doesn't exist on the target.
85 request = FakeLaunchpadRequest([], ['michael'])87 request = FakeLaunchpadRequest([], ["0"])
86 self.assertRaises(88 self.assertRaises(
87 NotFound,89 NotFound,
88 self.navigation(self.target, request).publishTraverse,90 self.navigation(self.target, request).publishTraverse,
89 request, '+subscription')91 request, '+subscription')
9092
91 def test_missing_person_structural_subscription_traversal(self):93 def test_other_target_structural_subscription_traversal(self):
92 # Verify that a NotFound is raised when attempting to reach94 # Verify that a NotFound is raised when attempting to reach
93 # a structural subscription for a person that does not exist.95 # a structural subscription for a different target.
94 request = FakeLaunchpadRequest([], ['doesnotexist'])96 other_target = self.factory.makeProduct()
97 other_subscription = other_target.addBugSubscription(
98 self.eric, self.eric)
99 # To get an ID, we must commit the transaction.
100 transaction.commit()
101 request = FakeLaunchpadRequest([], [str(other_subscription.id)])
95 self.assertRaises(102 self.assertRaises(
96 NotFound,103 NotFound,
97 self.navigation(self.target, request).publishTraverse,104 self.navigation(self.target, request).publishTraverse,
@@ -100,9 +107,12 @@
100 def test_structural_subscription_canonical_url(self):107 def test_structural_subscription_canonical_url(self):
101 # Verify that the canonical_url of a structural subscription108 # Verify that the canonical_url of a structural subscription
102 # is correct.109 # is correct.
110 expected_url = (
111 canonical_url(self.target) + '/+subscription/' +
112 str(self.subscription.id))
103 self.assertEqual(113 self.assertEqual(
104 canonical_url(self.target.getSubscription(self.eric)),114 expected_url,
105 canonical_url(self.target) + '/+subscription/eric')115 canonical_url(self.target.getSubscription(self.eric)))
106116
107 def tearDown(self):117 def tearDown(self):
108 logout()118 logout()
@@ -364,6 +374,31 @@
364 self.view.target_label)374 self.view.target_label)
365375
366376
377class TestStructuralSubscriptionTargetAPI(TestCaseWithFactory):
378
379 layer = AppServerLayer
380
381 def setUp(self):
382 super(TestStructuralSubscriptionTargetAPI, self).setUp()
383 self.owner = self.factory.makePerson(name=u"foo")
384 self.subscription_target = self.factory.makeProduct(
385 owner=self.owner, name=u"bar")
386 with person_logged_in(self.owner):
387 self.subscription = self.subscription_target.addBugSubscription(
388 self.owner, self.owner)
389 transaction.commit()
390 self.service = self.factory.makeLaunchpadService(self.owner)
391 self.ws_subscription_target = ws_object(
392 self.service, self.subscription_target)
393 self.ws_subscription = ws_object(self.service, self.subscription)
394
395 def test_getSubscriptionByID(self):
396 self.assertEqual(
397 self.ws_subscription,
398 self.ws_subscription_target.getSubscriptionByID(
399 subscription_id=self.subscription.id))
400
401
367class TestStructuralSubscriptionAPI(TestCaseWithFactory):402class TestStructuralSubscriptionAPI(TestCaseWithFactory):
368403
369 layer = AppServerLayer404 layer = AppServerLayer
@@ -371,10 +406,10 @@
371 def setUp(self):406 def setUp(self):
372 super(TestStructuralSubscriptionAPI, self).setUp()407 super(TestStructuralSubscriptionAPI, self).setUp()
373 self.owner = self.factory.makePerson(name=u"foo")408 self.owner = self.factory.makePerson(name=u"foo")
374 self.structure = self.factory.makeProduct(409 self.subscription_target = self.factory.makeProduct(
375 owner=self.owner, name=u"bar")410 owner=self.owner, name=u"bar")
376 with person_logged_in(self.owner):411 with person_logged_in(self.owner):
377 self.subscription = self.structure.addBugSubscription(412 self.subscription = self.subscription_target.addBugSubscription(
378 self.owner, self.owner)413 self.owner, self.owner)
379 transaction.commit()414 transaction.commit()
380 self.service = self.factory.makeLaunchpadService(self.owner)415 self.service = self.factory.makeLaunchpadService(self.owner)
381416
=== modified file 'lib/lp/bugs/interfaces/structuralsubscription.py'
--- lib/lp/bugs/interfaces/structuralsubscription.py 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/interfaces/structuralsubscription.py 2011-01-31 09:32:26 +0000
@@ -15,10 +15,6 @@
15 'IStructuralSubscriptionTargetHelper',15 'IStructuralSubscriptionTargetHelper',
16 ]16 ]
1717
18from lazr.enum import (
19 DBEnumeratedType,
20 DBItem,
21 )
22from lazr.restful.declarations import (18from lazr.restful.declarations import (
23 call_with,19 call_with,
24 export_as_webservice_entry,20 export_as_webservice_entry,
@@ -58,7 +54,7 @@
58class IStructuralSubscriptionPublic(Interface):54class IStructuralSubscriptionPublic(Interface):
59 """The public parts of a subscription to a Launchpad structure."""55 """The public parts of a subscription to a Launchpad structure."""
6056
61 id = Int(title=_('ID'), readonly=True, required=True)57 id = exported(Int(title=_('ID'), readonly=True, required=True))
62 product = Int(title=_('Product'), required=False, readonly=True)58 product = Int(title=_('Product'), required=False, readonly=True)
63 productseries = Int(59 productseries = Int(
64 title=_('Product series'), required=False, readonly=True)60 title=_('Product series'), required=False, readonly=True)
@@ -151,7 +147,14 @@
151 @operation_returns_entry(IStructuralSubscription)147 @operation_returns_entry(IStructuralSubscription)
152 @export_read_operation()148 @export_read_operation()
153 def getSubscription(person):149 def getSubscription(person):
154 """Return the subscription for `person`, if it exists."""150 """Return a subscriptions for a `person`."""
151
152 @operation_parameters(
153 subscription_id=Int(title=_("Subscription ID"), required=True))
154 @operation_returns_entry(IStructuralSubscription)
155 @export_read_operation()
156 def getSubscriptionByID(subscription_id):
157 """Return a StructuralSubscription with ID `subscription_id`."""
155158
156 target_type_display = Attribute("The type of the target, for display.")159 target_type_display = Attribute("The type of the target, for display.")
157160
158161
=== modified file 'lib/lp/bugs/model/structuralsubscription.py'
--- lib/lp/bugs/model/structuralsubscription.py 2011-01-22 02:59:35 +0000
+++ lib/lp/bugs/model/structuralsubscription.py 2011-01-31 09:32:26 +0000
@@ -444,10 +444,16 @@
444 all_subscriptions = self.getSubscriptions(subscriber=person)444 all_subscriptions = self.getSubscriptions(subscriber=person)
445 return all_subscriptions.one()445 return all_subscriptions.one()
446446
447 def getSubscriptionByID(self, subscription_id):
448 """See `IStructuralSubscriptionTarget`."""
449 subscriptions = self.getSubscriptions(
450 subscription_id=subscription_id)
451 return subscriptions.one()
452
447 def getSubscriptions(self,453 def getSubscriptions(self,
448 min_bug_notification_level=454 min_bug_notification_level=
449 BugNotificationLevel.NOTHING,455 BugNotificationLevel.NOTHING,
450 subscriber=None):456 subscriber=None, subscription_id=None):
451 """See `IStructuralSubscriptionTarget`."""457 """See `IStructuralSubscriptionTarget`."""
452 from lp.registry.model.person import Person458 from lp.registry.model.person import Person
453 clauses = [459 clauses = [
@@ -463,6 +469,10 @@
463 clauses.append(469 clauses.append(
464 StructuralSubscription.subscriberID==subscriber.id)470 StructuralSubscription.subscriberID==subscriber.id)
465471
472 if subscription_id is not None:
473 clauses.append(
474 StructuralSubscription.id==subscription_id)
475
466 store = Store.of(self.__helper.pillar)476 store = Store.of(self.__helper.pillar)
467 return store.find(477 return store.find(
468 StructuralSubscription, *clauses).order_by('Person.displayname')478 StructuralSubscription, *clauses).order_by('Person.displayname')
469479
=== modified file 'lib/lp/bugs/tests/test_structuralsubscriptiontarget.py'
--- lib/lp/bugs/tests/test_structuralsubscriptiontarget.py 2011-01-21 08:12:29 +0000
+++ lib/lp/bugs/tests/test_structuralsubscriptiontarget.py 2011-01-31 09:32:26 +0000
@@ -171,6 +171,33 @@
171 self.team, self.team_owner),171 self.team, self.team_owner),
172 None)172 None)
173173
174 def test_getSubscriptionByID_nothing(self):
175 login_person(self.team_owner)
176 self.assertIs(
177 None,
178 self.target.getSubscriptionByID(0))
179
180 def test_getSubscriptionByID_success(self):
181 login_person(self.team_owner)
182 # Create a subscription to return.
183 subscription = self.target.addBugSubscription(
184 self.team, self.team_owner)
185 self.assertEquals(
186 subscription,
187 self.target.getSubscriptionByID(subscription.id))
188
189 def test_getSubscriptionByID_other_target(self):
190 # No subscription is returned if one tries to get
191 # a subscription from a different target.
192 login_person(self.team_owner)
193 # Create a subscription on a different target.
194 other_target = self.factory.makeProduct()
195 subscription = other_target.addBugSubscription(
196 self.team, self.team_owner)
197 self.assertIs(
198 None,
199 self.target.getSubscriptionByID(subscription.id))
200
174201
175class FilteredStructuralSubscriptionTestBase:202class FilteredStructuralSubscriptionTestBase:
176 """Tests for filtered structural subscriptions."""203 """Tests for filtered structural subscriptions."""
177204
=== modified file 'lib/lp/registry/stories/person/xx-person-subscriptions.txt'
--- lib/lp/registry/stories/person/xx-person-subscriptions.txt 2011-01-13 16:28:49 +0000
+++ lib/lp/registry/stories/person/xx-person-subscriptions.txt 2011-01-31 09:32:26 +0000
@@ -236,9 +236,9 @@
236 ... "http://launchpad.dev/people/+me/+structural-subscriptions")236 ... "http://launchpad.dev/people/+me/+structural-subscriptions")
237 >>> show_create_links(admin_browser)237 >>> show_create_links(admin_browser)
238 mozilla-firefox in ubuntu238 mozilla-firefox in ubuntu
239 * Create a new filter --> /ubuntu/.../name16/+new-filter239 * Create a new filter --> /ubuntu/.../+new-filter
240 pmount in ubuntu240 pmount in ubuntu
241 * Create a new filter --> /ubuntu/.../name16/+new-filter241 * Create a new filter --> /ubuntu/.../+new-filter
242242
243If the user does not have the necessary rights to create new bug243If the user does not have the necessary rights to create new bug
244filters the "Create" link is not shown.244filters the "Create" link is not shown.
245245
=== modified file 'lib/lp/registry/stories/webservice/xx-structuralsubscription.txt'
--- lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2010-12-21 23:19:35 +0000
+++ lib/lp/registry/stories/webservice/xx-structuralsubscription.txt 2011-01-31 09:32:26 +0000
@@ -1,4 +1,5 @@
1= Structural Subscriptions =1Structural Subscriptions
2========================
23
3Structural subscriptions can be obtained from any target: a project,4Structural subscriptions can be obtained from any target: a project,
4project series, project group, distribution, distribution series or5project series, project group, distribution, distribution series or
@@ -37,7 +38,7 @@
37 ... '/fooix', 'addBugSubscription')38 ... '/fooix', 'addBugSubscription')
38 HTTP/1.1 201 Created39 HTTP/1.1 201 Created
39 ...40 ...
40 Location: http://.../fooix/+subscription/eric41 Location: http://.../fooix/+subscription/...
41 ...42 ...
4243
43 >>> subscriptions = webservice.named_get(44 >>> subscriptions = webservice.named_get(
@@ -46,11 +47,12 @@
46 start: 047 start: 0
47 total_size: 148 total_size: 1
48 ---49 ---
49 bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters'50 bug_filters_collection_link: u'.../fooix/+subscription/.../bug_filters'
50 date_created: u'...'51 date_created: u'...'
51 date_last_updated: u'...'52 date_last_updated: u'...'
53 id: ...
52 resource_type_link: u'http://.../#structural_subscription'54 resource_type_link: u'http://.../#structural_subscription'
53 self_link: u'http://.../fooix/+subscription/eric'55 self_link: u'http://.../fooix/+subscription/...'
54 subscribed_by_link: u'http://.../~eric'56 subscribed_by_link: u'http://.../~eric'
55 subscriber_link: u'http://.../~eric'57 subscriber_link: u'http://.../~eric'
56 target_link: u'http://.../fooix'58 target_link: u'http://.../fooix'
@@ -61,11 +63,12 @@
61 >>> pprint_entry(eric_webservice.named_get(63 >>> pprint_entry(eric_webservice.named_get(
62 ... '/fooix', 'getSubscription',64 ... '/fooix', 'getSubscription',
63 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())65 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
64 bug_filters_collection_link: u'.../fooix/+subscription/eric/bug_filters'66 bug_filters_collection_link: u'.../fooix/+subscription/.../bug_filters'
65 date_created: u'...'67 date_created: u'...'
66 date_last_updated: u'...'68 date_last_updated: u'...'
69 id: ...
67 resource_type_link: u'http://.../#structural_subscription'70 resource_type_link: u'http://.../#structural_subscription'
68 self_link: u'http://.../fooix/+subscription/eric'71 self_link: u'http://.../fooix/+subscription/...'
69 subscribed_by_link: u'http://.../~eric'72 subscribed_by_link: u'http://.../~eric'
70 subscriber_link: u'http://.../~eric'73 subscriber_link: u'http://.../~eric'
71 target_link: u'http://.../fooix'74 target_link: u'http://.../fooix'
@@ -96,7 +99,8 @@
96 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))99 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
97 HTTP/1.1 401 Unauthorized100 HTTP/1.1 401 Unauthorized
98 ...101 ...
99 UserCannotSubscribePerson: eric does not have permission to subscribe pythons.102 UserCannotSubscribePerson: eric does not have permission
103 to subscribe pythons.
100 <BLANKLINE>104 <BLANKLINE>
101105
102Oops, Eric isn't a team admin. Eric gets Michael to try, since he is an106Oops, Eric isn't a team admin. Eric gets Michael to try, since he is an
@@ -110,7 +114,7 @@
110 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))114 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
111 HTTP/1.1 201 Created115 HTTP/1.1 201 Created
112 ...116 ...
113 Location: http://.../fooix/+subscription/pythons117 Location: http://.../fooix/+subscription/...
114 ...118 ...
115119
116 >>> subscriptions = webservice.named_get(120 >>> subscriptions = webservice.named_get(
@@ -119,11 +123,12 @@
119 start: 0123 start: 0
120 total_size: 1124 total_size: 1
121 ---125 ---
122 bug_filters_collection_link: u'.../fooix/+subscription/pythons/bug_filters'126 bug_filters_collection_link: u'.../fooix/+subscription/.../bug_filters'
123 date_created: u'...'127 date_created: u'...'
124 date_last_updated: u'...'128 date_last_updated: u'...'
129 id: ...
125 resource_type_link: u'http://.../#structural_subscription'130 resource_type_link: u'http://.../#structural_subscription'
126 self_link: u'http://.../fooix/+subscription/pythons'131 self_link: u'http://.../fooix/+subscription/...'
127 subscribed_by_link: u'http://.../~michael'132 subscribed_by_link: u'http://.../~michael'
128 subscriber_link: u'http://.../~pythons'133 subscriber_link: u'http://.../~pythons'
129 target_link: u'http://.../fooix'134 target_link: u'http://.../fooix'
@@ -136,7 +141,8 @@
136 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))141 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
137 HTTP/1.1 401 Unauthorized142 HTTP/1.1 401 Unauthorized
138 ...143 ...
139 UserCannotSubscribePerson: eric does not have permission to unsubscribe pythons.144 UserCannotSubscribePerson: eric does not have permission
145 to unsubscribe pythons.
140 <BLANKLINE>146 <BLANKLINE>
141147
142Michael can, though.148Michael can, though.

Subscribers

People subscribed via source and target branches

to status/vote changes: