Merge lp:~wallyworld/launchpad/blueprint-subscription-forms into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 13273
Proposed branch: lp:~wallyworld/launchpad/blueprint-subscription-forms
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/blueprint-subscriptions-tales-refactor
Diff against target: 596 lines (+188/-155)
8 files modified
lib/lp/blueprints/browser/configure.zcml (+8/-2)
lib/lp/blueprints/browser/specification.py (+12/-49)
lib/lp/blueprints/browser/specificationsubscription.py (+79/-16)
lib/lp/blueprints/stories/standalone/subscribing.txt (+60/-19)
lib/lp/blueprints/templates/specification-subscriber-row.pt (+2/-2)
lib/lp/blueprints/templates/specification-subscription-delete.pt (+18/-0)
lib/lp/blueprints/templates/specification-subscription.pt (+2/-64)
lib/lp/blueprints/tests/test_webservice.py (+7/-3)
To merge this branch: bzr merge lp:~wallyworld/launchpad/blueprint-subscription-forms
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+65184@code.launchpad.net

Commit message

[r=benji][bug=50875] Allow teams to be unsubscribed from blueprints and fix (modernise) form infrastructure.

Description of the change

This branch builds on the blueprint refactoring work in the previous branch to add support for unsubscribing teams from blueprints. It also replaces old blueprint form code with the preferred LaunchpadFormView based infrastructure.

== Implementation ==

Blueprint subscription forms were still implemented using the old form infrastructure which relied on processing html posts in the view initialise() method. The code was modernised to use @action and LaunchpadFormView for managing subscriptions. A few artifacts of the implementation:
- replace two different forms and code used to modify a subscription with a single implementation
- split the "all in one" form used for managing one's own subscriptions into separate add/modify/delete forms
- use the same forms as above to handle subscribing someone else
- consistent use of "Subscribe", "Unsubscribe" etc for the form buttons as opposed to "Subscribe"/"Continue" in different places
- better informational messages - the name of the "someone else" is included in the info message when a different user of subscribed or unsubscribed

The above html changes will make it easier to do the ajax implementation consistent with the rest of lp. To reiterate - this branch does the work to provide the html forms based implementation.

For both the current logged in user, and any allowed teams, unsubscribing is done by clicking the red "remove" icon to the right of the user name. Editing a subscription is done by clicking the icon to the left of the user name.

== Demo ==

http://people.canonical.com/~ianb/blueprint-subscription-portal1.png

The screenshot shows the result of subscribing the Launchpad Administrators team. See the info message and the entry in the portal. The admins team can be unsubscribed hence the remove icon, but the other team and user shown in the portal cannot be subscribed so there's no option to do it.

== Tests ==

Blueprint subscriptions are tested by doc tests in subscribing.txt. These were modified to account for the changed button names as well as adding a test for unsubscribing teams and tests for the better informational messages.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/blueprints/browser/specification.py
  lib/lp/blueprints/browser/specificationsubscription.py
  lib/lp/blueprints/stories/standalone/subscribing.txt
  lib/lp/blueprints/templates/specification-subscriber-row.pt
  lib/lp/blueprints/templates/specification-subscription-delete.pt
  lib/lp/blueprints/templates/specification-subscription.pt

./lib/lp/blueprints/browser/specification.py
     203: E251 no spaces around keyword / parameter equals
./lib/lp/blueprints/stories/standalone/subscribing.txt
     128: source exceeds 78 characters.
     246: source exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. I had a couple of thoughts while reading over
it, but nothing that would keep us from landing it.

In specificationsubscription.py (line 221 of the diff), would it be
better to say "Modify subscription to %s" (adding the "to")?

I also noticed that the elements of __all__ in
specificationsubscription.py aren't quite in alphabetical order.

This test confuses me:

Or we can click the icon next to their name to get to the subscription edit
page.

    >>> browser.getLink(url='/+subscription/stub').click()
    >>> browser.getControl(name='field.essential').value = 'no'
    >>> browser.getControl('Change').click()

I don't see any assertions. Maybe instead of the second and third lines
there should be a check to be sure the current URL is what is expected.

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Benji

Thanks for the review.

>
> In specificationsubscription.py (line 221 of the diff), would it be
> better to say "Modify subscription to %s" (adding the "to")?
>
> I also noticed that the elements of __all__ in
> specificationsubscription.py aren't quite in alphabetical order.
>

Will fix.

> This test confuses me:
>
> Or we can click the icon next to their name to get to the subscription edit
> page.
>
> >>> browser.getLink(url='/+subscription/stub').click()
> >>> browser.getControl(name='field.essential').value = 'no'
> >>> browser.getControl('Change').click()
>
> I don't see any assertions. Maybe instead of the second and third lines
> there should be a check to be sure the current URL is what is expected.
>

There was an existing doc test that simply did the above that I reused.
Yes, it should have an assert (as should the existing test). I'll add
one in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/blueprints/browser/configure.zcml'
--- lib/lp/blueprints/browser/configure.zcml 2011-06-21 12:03:49 +0000
+++ lib/lp/blueprints/browser/configure.zcml 2011-06-21 12:03:51 +0000
@@ -335,10 +335,16 @@
335 <browser:page335 <browser:page
336 for="lp.blueprints.interfaces.specification.ISpecification"336 for="lp.blueprints.interfaces.specification.ISpecification"
337 name="+subscribe"337 name="+subscribe"
338 class="lp.blueprints.browser.specification.SpecificationSubscriptionView"338 class="lp.blueprints.browser.specificationsubscription.SpecificationSubscriptionAddView"
339 permission="launchpad.AnyPerson"339 permission="launchpad.AnyPerson"
340 template="../templates/specification-subscription.pt"/>340 template="../templates/specification-subscription.pt"/>
341 <browser:page341 <browser:page
342 for="lp.blueprints.interfaces.specification.ISpecificationSubscription"
343 name="+unsubscribe"
344 class="lp.blueprints.browser.specificationsubscription.SpecificationSubscriptionDeleteView"
345 permission="launchpad.Edit"
346 template="../templates/specification-subscription-delete.pt"/>
347 <browser:page
342 for="lp.blueprints.interfaces.specification.ISpecification"348 for="lp.blueprints.interfaces.specification.ISpecification"
343 name="+givefeedback"349 name="+givefeedback"
344 class="lp.blueprints.browser.specificationfeedback.SpecificationFeedbackClearingView"350 class="lp.blueprints.browser.specificationfeedback.SpecificationFeedbackClearingView"
@@ -347,7 +353,7 @@
347 <browser:page353 <browser:page
348 name="+addsubscriber"354 name="+addsubscriber"
349 for="lp.blueprints.interfaces.specification.ISpecification"355 for="lp.blueprints.interfaces.specification.ISpecification"
350 class="lp.blueprints.browser.specificationsubscription.SpecificationSubscriptionAddView"356 class="lp.blueprints.browser.specificationsubscription.SpecificationSubscriptionAddSubscriberView"
351 permission="launchpad.AnyPerson"357 permission="launchpad.AnyPerson"
352 template="../../app/templates/generic-edit.pt"/>358 template="../../app/templates/generic-edit.pt"/>
353 <browser:page359 <browser:page
354360
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py 2011-06-21 12:03:49 +0000
+++ lib/lp/blueprints/browser/specification.py 2011-06-21 12:03:51 +0000
@@ -15,27 +15,26 @@
15 'NewSpecificationFromSprintView',15 'NewSpecificationFromSprintView',
16 'SpecificationActionMenu',16 'SpecificationActionMenu',
17 'SpecificationContextMenu',17 'SpecificationContextMenu',
18 'SpecificationNavigation',
19 'SpecificationView',
20 'SpecificationSimpleView',
21 'SpecificationEditMilestoneView',18 'SpecificationEditMilestoneView',
22 'SpecificationEditPeopleView',19 'SpecificationEditPeopleView',
23 'SpecificationEditPriorityView',20 'SpecificationEditPriorityView',
24 'SpecificationEditStatusView',21 'SpecificationEditStatusView',
25 'SpecificationEditView',22 'SpecificationEditView',
26 'SpecificationEditWhiteboardView',23 'SpecificationEditWhiteboardView',
24 'SpecificationGoalDecideView',
27 'SpecificationGoalProposeView',25 'SpecificationGoalProposeView',
28 'SpecificationGoalDecideView',
29 'SpecificationLinkBranchView',26 'SpecificationLinkBranchView',
27 'SpecificationNavigation',
30 'SpecificationProductSeriesGoalProposeView',28 'SpecificationProductSeriesGoalProposeView',
31 'SpecificationRetargetingView',29 'SpecificationRetargetingView',
30 'SpecificationSetView',
31 'SpecificationSimpleView',
32 'SpecificationSprintAddView',32 'SpecificationSprintAddView',
33 'SpecificationSubscriptionView',
34 'SpecificationSupersedingView',33 'SpecificationSupersedingView',
35 'SpecificationTreePNGView',34 'SpecificationTreePNGView',
36 'SpecificationTreeImageTag',35 'SpecificationTreeImageTag',
37 'SpecificationTreeDotOutput',36 'SpecificationTreeDotOutput',
38 'SpecificationSetView',37 'SpecificationView',
39 ]38 ]
4039
41from operator import attrgetter40from operator import attrgetter
@@ -473,15 +472,14 @@
473 """Return the 'Edit Subscription' Link."""472 """Return the 'Edit Subscription' Link."""
474 user = self.user473 user = self.user
475 if user is None:474 if user is None:
476 text = 'Edit subscription'475 return Link('+subscribe', 'Edit subscription', icon='edit')
477 icon = 'edit'476
478 elif self.context.isSubscribed(user):477 if self.context.isSubscribed(user):
479 text = 'Update subscription'478 return Link(
480 icon = 'edit'479 '+subscription/%s' % user.name,
480 'Update subscription', icon='edit')
481 else:481 else:
482 text = 'Subscribe'482 return Link('+subscribe', 'Subscribe', icon='add')
483 icon = 'add'
484 return Link('+subscribe', text, icon=icon)
485483
486 @enabled_with_permission('launchpad.AnyPerson')484 @enabled_with_permission('launchpad.AnyPerson')
487 def linkbug(self):485 def linkbug(self):
@@ -539,13 +537,6 @@
539 return []537 return []
540 return list(self.context.getFeedbackRequests(self.user))538 return list(self.context.getFeedbackRequests(self.user))
541539
542 @property
543 def subscription(self):
544 """whether the current user has a subscription to the spec."""
545 if self.user is None:
546 return None
547 return self.context.subscription(self.user)
548
549 @cachedproperty540 @cachedproperty
550 def has_dep_tree(self):541 def has_dep_tree(self):
551 return self.context.dependencies or self.context.blocked_specs542 return self.context.dependencies or self.context.blocked_specs
@@ -578,25 +569,6 @@
578 if not self.user:569 if not self.user:
579 return570 return
580571
581 request = self.request
582 if request.method == 'POST':
583 # establish if a subscription form was posted.
584 sub = request.form.get('subscribe')
585 upd = request.form.get('update')
586 unsub = request.form.get('unsubscribe')
587 essential = request.form.get('essential') == 'yes'
588 if sub is not None:
589 self.context.subscribe(self.user, self.user, essential)
590 self.notices.append(
591 "You have subscribed to this blueprint.")
592 elif upd is not None:
593 self.context.subscribe(self.user, self.user, essential)
594 self.notices.append('Your subscription has been updated.')
595 elif unsub is not None:
596 self.context.unsubscribe(self.user, self.user)
597 self.notices.append(
598 "You have unsubscribed from this blueprint.")
599
600 if self.feedbackrequests:572 if self.feedbackrequests:
601 msg = "You have %d feedback request(s) on this blueprint."573 msg = "You have %d feedback request(s) on this blueprint."
602 msg %= len(self.feedbackrequests)574 msg %= len(self.feedbackrequests)
@@ -680,15 +652,6 @@
680 header='Change approval of basic direction')652 header='Change approval of basic direction')
681653
682654
683class SpecificationSubscriptionView(SpecificationView):
684
685 @property
686 def label(self):
687 if self.subscription is not None:
688 return "Modify subscription"
689 return "Subscribe to blueprint"
690
691
692class SpecificationEditSchema(ISpecification):655class SpecificationEditSchema(ISpecification):
693 """Provide overrides for the implementaion and definition status."""656 """Provide overrides for the implementaion and definition status."""
694657
695658
=== modified file 'lib/lp/blueprints/browser/specificationsubscription.py'
--- lib/lp/blueprints/browser/specificationsubscription.py 2011-06-21 12:03:49 +0000
+++ lib/lp/blueprints/browser/specificationsubscription.py 2011-06-21 12:03:51 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'SpecificationSubscriptionAddView',8 'SpecificationSubscriptionAddView',
9 'SpecificationSubscriptionAddSubscriberView',
9 'SpecificationSubscriptionEditView',10 'SpecificationSubscriptionEditView',
10 ]11 ]
1112
@@ -29,41 +30,106 @@
29from lp.blueprints.interfaces.specificationsubscription import (30from lp.blueprints.interfaces.specificationsubscription import (
30 ISpecificationSubscription,31 ISpecificationSubscription,
31 )32 )
32from lp.registry.model.person import person_sort_key
33from lp.services.propertycache import cachedproperty33from lp.services.propertycache import cachedproperty
3434
3535
36class SpecificationSubscriptionAddView(LaunchpadFormView):36class SpecificationSubscriptionAddView(LaunchpadFormView):
37 """Used to subscribe the current user to a blueprint."""
3738
38 schema = ISpecificationSubscription39 schema = ISpecificationSubscription
40 field_names = ['essential']
41 label = 'Subscribe to blueprint'
42
43 @property
44 def cancel_url(self):
45 return canonical_url(self.context)
46
47 next_url = cancel_url
48
49 def _subscribe(self, person, essential):
50 self.context.subscribe(person, self.user, essential)
51
52 @action(_('Subscribe'), name='subscribe')
53 def subscribe_action(self, action, data):
54 self._subscribe(self.user, data['essential'])
55 self.request.response.addInfoNotification(
56 "You have subscribed to this blueprint.")
57
58
59class SpecificationSubscriptionAddSubscriberView(
60 SpecificationSubscriptionAddView):
61 """Used to subscribe someone else to a blueprint."""
62
39 field_names = ['person', 'essential']63 field_names = ['person', 'essential']
40 label = 'Subscribe someone else'64 label = 'Subscribe someone else'
41 for_input = True65 for_input = True
4266
43 @action(_('Continue'), name='continue')67 @action(_('Subscribe'), name='subscribe')
44 def continue_action(self, action, data):68 def subscribe_action(self, action, data):
45 self.context.subscribe(data['person'], self.user, data['essential'])69 person = data['person']
46 self.next_url = canonical_url(self.context)70 self._subscribe(person, data['essential'])
71 self.request.response.addInfoNotification(
72 "%s has been subscribed to this blueprint." % person.displayname)
73
74
75class SpecificationSubscriptionDeleteView(LaunchpadFormView):
76 """Used to unsubscribe someone from a blueprint."""
77
78 schema = ISpecificationSubscription
79 field_names = []
80
81 @property
82 def label(self):
83 return ("Unsubscribe %s from %s"
84 % (self.context.person.displayname,
85 self.context.specification.title))
86
87 page_title = label
4788
48 @property89 @property
49 def cancel_url(self):90 def cancel_url(self):
50 return canonical_url(self.context)91 return canonical_url(self.context.specification)
92
93 next_url = cancel_url
94
95 @action('Unsubscribe', name='unsubscribe')
96 def unsubscribe_action(self, action, data):
97 self.context.specification.unsubscribe(self.context.person, self.user)
98 if self.context.person == self.user:
99 self.request.response.addInfoNotification(
100 "You have unsubscribed from this blueprint.")
101 else:
102 self.request.response.addInfoNotification(
103 "%s has been unsubscribed from this blueprint."
104 % self.context.person.displayname)
51105
52106
53class SpecificationSubscriptionEditView(LaunchpadEditFormView):107class SpecificationSubscriptionEditView(LaunchpadEditFormView):
54108
55 schema = ISpecificationSubscription109 schema = ISpecificationSubscription
56 field_names = ['essential']110 field_names = ['essential']
57 label = 'Edit subscription'111
112 @property
113 def label(self):
114 return "Modify subscription to %s" % self.context.specification.title
115
116 @property
117 def cancel_url(self):
118 return canonical_url(self.context.specification)
119
120 next_url = cancel_url
58121
59 @action(_('Change'), name='change')122 @action(_('Change'), name='change')
60 def change_action(self, action, data):123 def change_action(self, action, data):
61 self.updateContextFromData(data)124 self.updateContextFromData(data)
62 self.next_url = canonical_url(self.context.specification)125 is_current_user_subscription = self.user == self.context.person
63126 if is_current_user_subscription:
64 @property127 self.request.response.addInfoNotification(
65 def cancel_url(self):128 "Your subscription has been updated.")
66 return canonical_url(self.context.specification)129 else:
130 self.request.response.addInfoNotification(
131 "The subscription for %s has been updated."
132 % self.context.person.displayname)
67133
68134
69class SpecificationPortletSubcribersContents(LaunchpadView):135class SpecificationPortletSubcribersContents(LaunchpadView):
@@ -81,12 +147,9 @@
81 The list is sorted such that subscriptions you can unsubscribe appear147 The list is sorted such that subscriptions you can unsubscribe appear
82 before all other subscriptions.148 before all other subscriptions.
83 """149 """
84 sort_key = lambda sub: person_sort_key(sub.person)
85 subscriptions = sorted(self.context.subscriptions, key=sort_key)
86
87 can_unsubscribe = []150 can_unsubscribe = []
88 cannot_unsubscribe = []151 cannot_unsubscribe = []
89 for subscription in subscriptions:152 for subscription in self.context.subscriptions:
90 if not check_permission('launchpad.View', subscription.person):153 if not check_permission('launchpad.View', subscription.person):
91 continue154 continue
92 if subscription.person == self.user:155 if subscription.person == self.user:
93156
=== modified file 'lib/lp/blueprints/stories/standalone/subscribing.txt'
--- lib/lp/blueprints/stories/standalone/subscribing.txt 2011-06-07 04:39:03 +0000
+++ lib/lp/blueprints/stories/standalone/subscribing.txt 2011-06-21 12:03:51 +0000
@@ -68,10 +68,6 @@
68the link to modify the subscription. It should currently be checked.68the link to modify the subscription. It should currently be checked.
6969
70 >>> submod_link.click()70 >>> submod_link.click()
71 >>> print browser.title
72 Modify subscription : Support E4X in EcmaScript :
73 Blueprints : Mozilla Firefox
74
75 >>> essential = browser.getControl('essential')71 >>> essential = browser.getControl('essential')
76 >>> essential.selected72 >>> essential.selected
77 True73 True
@@ -79,7 +75,7 @@
79We will unset the essential flag and resubmit:75We will unset the essential flag and resubmit:
8076
81 >>> essential.selected = False77 >>> essential.selected = False
82 >>> browser.getControl('Update').click()78 >>> browser.getControl('Change').click()
83 >>> 'Your subscription has been updated' in browser.contents79 >>> 'Your subscription has been updated' in browser.contents
84 True80 True
8581
@@ -99,15 +95,13 @@
99 True95 True
10096
101We don't really want to be subscribed, so lets unsubscribe from that97We don't really want to be subscribed, so lets unsubscribe from that
102spec. We load the subscription page, and now the button says98spec. We click the remove icon in the subscribers list, and now the
103"Unsubscribe".99unsubscribe confirmation page loads.
104100
105 >>> browser.getLink('Update subscription').click()101 >>> unsubit = browser.getLink(id='unsubscribe-subscriber-13')
106 >>> unsubit = browser.getControl(name='unsubscribe')
107 >>> unsubit.value
108 'Unsubscribe'
109
110 >>> unsubit.click()102 >>> unsubit.click()
103 >>> confirm = browser.getControl('Unsubscribe')
104 >>> confirm.click()
111 >>> 'You have unsubscribed from this blueprint.' in browser.contents105 >>> 'You have unsubscribed from this blueprint.' in browser.contents
112 True106 True
113107
@@ -130,7 +124,9 @@
130 'http://blueprints.launchpad.dev/firefox/+spec/e4x'124 'http://blueprints.launchpad.dev/firefox/+spec/e4x'
131125
132 >>> browser.getControl('Subscriber').value = 'stub'126 >>> browser.getControl('Subscriber').value = 'stub'
133 >>> browser.getControl('Continue').click()127 >>> browser.getControl('Subscribe').click()
128 >>> 'Stuart Bishop has been subscribed to this blueprint.' in browser.contents
129 True
134130
135When we subscribe someone else to a blueprint, they get notified by131When we subscribe someone else to a blueprint, they get notified by
136email.132email.
@@ -157,7 +153,18 @@
157 >>> browser.getLink('Subscribe someone else').click()153 >>> browser.getLink('Subscribe someone else').click()
158 >>> browser.getControl('Subscriber').value = 'stub'154 >>> browser.getControl('Subscriber').value = 'stub'
159 >>> browser.getControl(name='field.essential').value = 'yes'155 >>> browser.getControl(name='field.essential').value = 'yes'
160 >>> browser.getControl('Continue').click()156 >>> browser.getControl('Subscribe').click()
157
158We now check that the subscriptions portlet is showing the correct information
159based on the subscription change we have made above.
160
161 >>> subscribers = find_tags_by_class(browser.contents, 'subscriber')
162 >>> for subscriber in subscribers:
163 ... a_tags = subscriber.findAll('a')
164 ... img = a_tags[0].first('img')
165 ... print img['src'],
166 ... print a_tags[1].string
167 /@@/subscriber-essential Stuart Bishop
161168
162When we change a user's subscription, they get notified by email. Teams169When we change a user's subscription, they get notified by email. Teams
163can be subscribed to a blueprint too170can be subscribed to a blueprint too
@@ -166,6 +173,29 @@
166 >>> '[Participation essential]' in last_email.get_payload()173 >>> '[Participation essential]' in last_email.get_payload()
167 True174 True
168175
176We can click the icon next to a user's name to get to the subscription edit
177page.
178
179 >>> browser.getLink(url='/+subscription/stub').click()
180 >>> browser.getControl(name='field.essential').value = None
181 >>> browser.getControl('Change').click()
182
183We now check that the subscriptions portlet is showing the correct information
184based on the subscription change we have made above.
185
186 >>> subscribers = find_tags_by_class(browser.contents, 'subscriber')
187 >>> for subscriber in subscribers:
188 ... a_tags = subscriber.findAll('a')
189 ... img = a_tags[0].first('img')
190 ... print img['src'],
191 ... print a_tags[1].string
192 /@@/subscriber-inessential Stuart Bishop
193
194And check the email notification too.
195
196 >>> last_email = pop_notifications()[-1]
197 >>> '[Participation non-essential]' in last_email.get_payload()
198 True
169199
170Subscribing teams200Subscribing teams
171-----------------201-----------------
@@ -197,7 +227,7 @@
197 >>> browser.getLink('Subscribe someone else').click()227 >>> browser.getLink('Subscribe someone else').click()
198 >>> browser.getControl('Subscriber').value = 'admins'228 >>> browser.getControl('Subscriber').value = 'admins'
199 >>> browser.getControl(name='field.essential').value = None229 >>> browser.getControl(name='field.essential').value = None
200 >>> browser.getControl('Continue').click()230 >>> browser.getControl('Subscribe').click()
201231
202We created a subscription for the Launchpad Admins, but because the team232We created a subscription for the Launchpad Admins, but because the team
203does not have a preferred email address, an email is sent to each active233does not have a preferred email address, an email is sent to each active
@@ -213,7 +243,7 @@
213 >>> browser.getLink('Subscribe someone else').click()243 >>> browser.getLink('Subscribe someone else').click()
214 >>> browser.getControl('Subscriber').value = 'admins'244 >>> browser.getControl('Subscriber').value = 'admins'
215 >>> browser.getControl(name='field.essential').value = 'yes'245 >>> browser.getControl(name='field.essential').value = 'yes'
216 >>> browser.getControl('Continue').click()246 >>> browser.getControl('Subscribe').click()
217247
218We modified the Launchpad Admins team's subscription and again, an email248We modified the Launchpad Admins team's subscription and again, an email
219is sent to each active member.249is sent to each active member.
@@ -230,7 +260,18 @@
230 >>> browser.getLink('Subscribe someone else').click()260 >>> browser.getLink('Subscribe someone else').click()
231 >>> browser.getControl('Subscriber').value = 'ubuntu-team'261 >>> browser.getControl('Subscriber').value = 'ubuntu-team'
232 >>> browser.getControl(name='field.essential').value = None262 >>> browser.getControl(name='field.essential').value = None
233 >>> browser.getControl('Continue').click()263 >>> browser.getControl('Subscribe').click()
264
265Because the current logged in user carlos is a member of the admins team it is
266possible to unsubscribe the team. We click the remove icon in the subscribers
267list, and now the unsubscribe confirmation page loads.
268
269 >>> unsubit = browser.getLink(id='unsubscribe-subscriber-25')
270 >>> unsubit.click()
271 >>> confirm = browser.getControl('Unsubscribe')
272 >>> confirm.click()
273 >>> 'Launchpad Administrators has been unsubscribed from this blueprint.' in browser.contents
274 True
234275
235We subscribe the Ubuntu Team and an email is sent to the team's276We subscribe the Ubuntu Team and an email is sent to the team's
236preferred email address.277preferred email address.
@@ -245,7 +286,7 @@
245 >>> browser.getLink('Subscribe someone else').click()286 >>> browser.getLink('Subscribe someone else').click()
246 >>> browser.getControl('Subscriber').value = 'ubuntu-team'287 >>> browser.getControl('Subscriber').value = 'ubuntu-team'
247 >>> browser.getControl(name='field.essential').value = 'yes'288 >>> browser.getControl(name='field.essential').value = 'yes'
248 >>> browser.getControl('Continue').click()289 >>> browser.getControl('Subscribe').click()
249290
250We modified the Ubuntu Team's subscription and again, an email is sent291We modified the Ubuntu Team's subscription and again, an email is sent
251to the team's preferred email address.292to the team's preferred email address.
252293
=== modified file 'lib/lp/blueprints/templates/specification-subscriber-row.pt'
--- lib/lp/blueprints/templates/specification-subscriber-row.pt 2011-06-21 12:03:49 +0000
+++ lib/lp/blueprints/templates/specification-subscriber-row.pt 2011-06-21 12:03:51 +0000
@@ -39,10 +39,10 @@
39 />39 />
40 <a tal:define="user request/lp:person"40 <a tal:define="user request/lp:person"
41 tal:condition="python: subscription.canBeUnsubscribedByUser(user)"41 tal:condition="python: subscription.canBeUnsubscribedByUser(user)"
42 href="+subscribe"
43 tal:attributes="42 tal:attributes="
44 title string:Unsubscribe ${subscription/person/fmt:displayname};43 title string:Unsubscribe ${subscription/person/fmt:displayname};
45 id string:unsubscribe-${subscription/css_name};"44 id string:unsubscribe-${subscription/css_name};
45 href string:${subscription/fmt:url}/+unsubscribe;"
46 >46 >
47 <img class="unsub-icon" src="/@@/remove" alt="Remove"47 <img class="unsub-icon" src="/@@/remove" alt="Remove"
48 tal:attributes="id string:unsubscribe-icon-${subscription/css_name}" />48 tal:attributes="id string:unsubscribe-icon-${subscription/css_name}" />
4949
=== added file 'lib/lp/blueprints/templates/specification-subscription-delete.pt'
--- lib/lp/blueprints/templates/specification-subscription-delete.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/templates/specification-subscription-delete.pt 2011-06-21 12:03:51 +0000
@@ -0,0 +1,18 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad">
8 <body>
9 <div metal:fill-slot="main">
10 <p>
11 Are you sure you want to delete the subscription for
12 <strong tal:content="context/person/displayname">Person</strong>?
13 </p>
14 <div metal:use-macro="context/@@launchpad_form/form">
15 </div>
16 </div>
17 </body>
18</html>
019
=== modified file 'lib/lp/blueprints/templates/specification-subscription.pt'
--- lib/lp/blueprints/templates/specification-subscription.pt 2011-05-15 19:31:31 +0000
+++ lib/lp/blueprints/templates/specification-subscription.pt 2011-06-21 12:03:51 +0000
@@ -15,70 +15,8 @@
1515
16<div metal:fill-slot="main">16<div metal:fill-slot="main">
1717
18 <div class="top-portlet" tal:condition="view/subscription">18 <div metal:use-macro="context/@@launchpad_form/form">
1919 </div>
20 <p>
21 Choose &#8220;Unsubscribe&#8221; to remove your subscription to this
22 blueprint, or &#8220;Cancel&#8221; to return to the blueprint page.
23 </p>
24
25 </div>
26
27 <div class="top-portlet" tal:condition="not: view/subscription">
28
29 <p>
30 Choose &#8220;Subscribe&#8221; to subscribe to this blueprint,
31 or &#8220;Cancel&#8221; to return to the blueprint page.
32 </p>
33
34 </div>
35
36 <form action="." method="POST">
37
38 <div class="field">
39
40 <tal:subscribed condition="view/subscription">
41 <input type="checkbox" id="essential" name="essential"
42 value="yes" checked="yes"
43 tal:condition="view/subscription/essential" />
44 <input type="checkbox" id="essential" name="essential"
45 value="yes"
46 tal:condition="not: view/subscription/essential" />
47 </tal:subscribed>
48 <tal:not_subscribed condition="not: view/subscription">
49 <input type="checkbox" id="essential" name="essential"
50 value="yes" />
51 </tal:not_subscribed>
52
53 <label for="essential">Participation essential</label>
54
55 <p class="formHelp">
56 Check this box only if you are required to be included in all discussions about
57 this feature when you are attending sprints or meetings with this
58 feature on the agenda.
59 </p>
60
61 </div>
62
63 <div class="actions">
64 <input tal:condition="not: view/subscription"
65 type="submit"
66 name="subscribe"
67 value="Subscribe" />
68 <input tal:condition="view/subscription"
69 type="submit"
70 name="update"
71 value="Update" />
72 <input tal:condition="view/subscription"
73 type="submit"
74 name="unsubscribe"
75 value="Unsubscribe" />
76 <input type="submit"
77 name="cancel"
78 value="Cancel" />
79 </div>
80 </form>
81
82</div>20</div>
8321
84</body>22</body>
8523
=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
--- lib/lp/blueprints/tests/test_webservice.py 2011-06-21 12:03:49 +0000
+++ lib/lp/blueprints/tests/test_webservice.py 2011-06-21 12:03:51 +0000
@@ -29,14 +29,18 @@
2929
30class SpecificationWebserviceTestCase(TestCaseWithFactory):30class SpecificationWebserviceTestCase(TestCaseWithFactory):
3131
32 def getLaunchpadlib(self):
33 user = self.factory.makePerson()
34 return launchpadlib_for("testing", user, version='devel')
35
32 def getSpecOnWebservice(self, spec_object):36 def getSpecOnWebservice(self, spec_object):
33 launchpadlib = self.factory.makeLaunchpadService()37 launchpadlib = self.getLaunchpadlib()
34 return launchpadlib.load(38 return launchpadlib.load(
35 '/%s/+spec/%s' % (spec_object.target.name, spec_object.name))39 '/%s/+spec/%s' % (spec_object.target.name, spec_object.name))
3640
37 def getPillarOnWebservice(self, pillar_obj):41 def getPillarOnWebservice(self, pillar_obj):
38 # XXX: 2010-11-26, salgado, bug=681767: Can't use relative URLs here.42 # XXX: 2010-11-26, salgado, bug=681767: Can't use relative URLs here.
39 launchpadlib = self.factory.makeLaunchpadService()43 launchpadlib = self.getLaunchpadlib()
40 return launchpadlib.load(44 return launchpadlib.load(
41 str(launchpadlib._root_uri) + '/' + pillar_obj.name)45 str(launchpadlib._root_uri) + '/' + pillar_obj.name)
4246
@@ -300,7 +304,7 @@
300 db_spec = self.factory.makeBlueprint()304 db_spec = self.factory.makeBlueprint()
301 db_person = self.factory.makePerson()305 db_person = self.factory.makePerson()
302 db_spec.subscribe(person=db_person)306 db_spec.subscribe(person=db_person)
303 launchpad = self.factory.makeLaunchpadService()307 launchpad = self.factory.makeLaunchpadService(person=db_person)
304308
305 spec = ws_object(launchpad, db_spec)309 spec = ws_object(launchpad, db_spec)
306 person = ws_object(launchpad, db_person)310 person = ws_object(launchpad, db_person)