Merge lp:~gmb/launchpad/extract-subscription-view-bug-651240 into lp:launchpad

Proposed by Graham Binns on 2010-10-06
Status: Merged
Approved by: Graham Binns on 2010-10-06
Approved revision: no longer in the source branch.
Merged at revision: 11702
Proposed branch: lp:~gmb/launchpad/extract-subscription-view-bug-651240
Merge into: lp:launchpad
Diff against target: 618 lines (+276/-205)
7 files modified
lib/lp/bugs/browser/bugsubscription.py (+257/-2)
lib/lp/bugs/browser/bugtask.py (+7/-197)
lib/lp/bugs/browser/configure.zcml (+1/-1)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt (+2/-2)
lib/lp/bugs/templates/bug-subscription.pt (+1/-1)
lib/lp/registry/browser/person.py (+7/-1)
lib/lp/registry/stories/person/xx-person-subscriptions.txt (+1/-1)
To merge this branch: bzr merge lp:~gmb/launchpad/extract-subscription-view-bug-651240
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2010-10-06 Approve on 2010-10-06
Review via email: mp+37713@code.launchpad.net

Commit Message

The Bug:+subscribe functionality has been moved into its own view class.

Description of the Change

This branch fixes bug 651240 by moving the +subscription-specific code
out of BugTaskView (where it has been since pretty much the dawn of
time) and into its own view. The majority of changes in this branch are
code moves as a result.

== lib/lp/bugs/browser/bugsubscription.py ==

 - I've added a new view, BugSubscriptionSubscribeSelfView. This does
   all the work that BugTask used to do when it was hit via the
   +subscription URL.

== lib/lp/bugs/browser/bugtask.py ==

 - There's an XXX here that I need to discuss with lifeless. Basically,
   the functionally empty for loop actually makes the view more
   efficient (in terms of queries run anyway) because, I presume, it
   causes the materialised objects to be cached by Storm. Removing it
   causes the test that ensures that querie counts remain reasonable to
   fail. I will hopefully remove this XXX-and-for-loop before landing,
   but it seems silly to block the review because of it.
 - I've moved all the +subscribe functionality out of BugTaskView.

== lib/lp/bugs/browser/configure.zcml ==

 - I've updated the ZCML to reflect the code move.

== lib/lp/bugs/templates/bug-subscription.pt ==

 - I've altered the form's action. For some reason the old version
   caused the view to break, though I've no idea why (basically it would
   never actually do anything with action=".". Suggestions as to reasons
   for this are welcome). Note that this form is going away in a
   subsequent branch and being replaced with the launchpadform macro.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

Graham, I have a few thoughts for you.

Firstly, consider (by consider I mean 'I really think you should do
this') writing specific tests for query scaling for the new view.

Secondly, the empty loop can be rephrased as
list(bug.getSubscribersForPerson(self.user))

This is eager loading the subscriber data.

*if* you arrange for the new view to be initialized before doing any
dereferencing of people in the main view, you can just trigger that
eager load in the subscribers view. If you can't arrange that, then
you've got two bugs:
1) you're adding a new query that isn't needed (you don't see this
because the scaling tests have a little fat - but check the counts
before and after, you'll see the extra query)
2) its being run too late to eager load anything.

you could make getSubscribersForPerson return a cached object, but
frankly I suspect that the problem is zope's wiring up of views and
subordinate things : death by a thousand cuts is a designed in feature
there, so moving all use of subscribers out of the main view is likely
the only way out of this.

Clearly this needs fixing - we don't want to make the page slower,
which this patch will do at the moment.

-Rob

Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
2--- lib/lp/bugs/browser/bugsubscription.py 2010-09-24 14:13:50 +0000
3+++ lib/lp/bugs/browser/bugsubscription.py 2010-10-07 15:32:53 +0000
4@@ -10,9 +10,21 @@
5 'BugSubscriptionAddView',
6 ]
7
8+import cgi
9+
10 from lazr.delegates import delegates
11 from simplejson import dumps
12
13+from zope.app.form import CustomWidgetFactory
14+from zope.app.form.browser.itemswidgets import RadioWidget
15+from zope.app.form.interfaces import IInputWidget
16+from zope.app.form.utility import setUpWidget
17+from zope.schema import Choice
18+from zope.schema.vocabulary import (
19+ SimpleTerm,
20+ SimpleVocabulary,
21+ )
22+
23 from canonical.launchpad.webapp import (
24 action,
25 canonical_url,
26@@ -20,6 +32,8 @@
27 LaunchpadView,
28 )
29 from canonical.launchpad.webapp.authorization import check_permission
30+from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
31+from canonical.launchpad.webapp.menu import structured
32 from lp.bugs.browser.bug import BugViewMixin
33 from lp.bugs.interfaces.bugsubscription import IBugSubscription
34
35@@ -44,8 +58,8 @@
36 message = '%s team has been subscribed to this bug.'
37 else:
38 message = '%s has been subscribed to this bug.'
39- self.request.response.addInfoNotification(message %
40- person.displayname)
41+ self.request.response.addInfoNotification(
42+ message % person.displayname)
43
44 @property
45 def next_url(self):
46@@ -60,6 +74,247 @@
47 page_title = label
48
49
50+class BugSubscriptionSubscribeSelfView(LaunchpadView,
51+ ReturnToReferrerMixin):
52+ """A view to handle the +subscribe page for a bug."""
53+
54+ @property
55+ def next_url(self):
56+ """Provided so returning to the page they came from works."""
57+ referer = self.request.getHeader('referer')
58+
59+ # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
60+ # browser setting referer to localhost.
61+ if referer and referer != 'localhost':
62+ next_url = referer
63+ else:
64+ next_url = canonical_url(self.context)
65+ return next_url
66+
67+ cancel_url = next_url
68+
69+ def initialize(self):
70+ """Set up the needed widgets."""
71+ bug = self.context.bug
72+
73+ # See render() for how this flag is used.
74+ self._redirecting_to_bug_list = False
75+
76+ if self.user is None:
77+ return
78+
79+ # Set up widgets in order to handle subscription requests.
80+ subscription_terms = []
81+ self_subscribed = False
82+ for person in bug.getSubscribersForPerson(self.user):
83+ if person.id == self.user.id:
84+ subscription_terms.append(
85+ SimpleTerm(
86+ person, person.name,
87+ 'Unsubscribe me from this bug'))
88+ self_subscribed = True
89+ else:
90+ subscription_terms.append(
91+ SimpleTerm(
92+ person, person.name,
93+ 'Unsubscribe <a href="%s">%s</a> from this bug' % (
94+ canonical_url(person),
95+ cgi.escape(person.displayname))))
96+ if not self_subscribed:
97+ subscription_terms.insert(0,
98+ SimpleTerm(
99+ self.user, self.user.name, 'Subscribe me to this bug'))
100+ subscription_vocabulary = SimpleVocabulary(subscription_terms)
101+ person_field = Choice(
102+ __name__='subscription',
103+ vocabulary=subscription_vocabulary, required=True)
104+ self.subscription_widget = CustomWidgetFactory(RadioWidget)
105+ setUpWidget(
106+ self, 'subscription', person_field, IInputWidget, value=self.user)
107+
108+ self.handleSubscriptionRequest()
109+
110+ def userIsSubscribed(self):
111+ """Is the user subscribed to this bug?"""
112+ return (
113+ self.context.bug.isSubscribed(self.user) or
114+ self.context.bug.isSubscribedToDupes(self.user))
115+
116+ def shouldShowUnsubscribeFromDupesWarning(self):
117+ """Should we warn the user about unsubscribing and duplicates?
118+
119+ The warning should tell the user that, when unsubscribing, they
120+ will also be unsubscribed from dupes of this bug.
121+ """
122+ if self.userIsSubscribed():
123+ return True
124+
125+ bug = self.context.bug
126+ for team in self.user.teams_participated_in:
127+ if bug.isSubscribed(team) or bug.isSubscribedToDupes(team):
128+ return True
129+
130+ return False
131+
132+ def render(self):
133+ """Render the bug list if the user has permission to see the bug."""
134+ # Prevent normal rendering when redirecting to the bug list
135+ # after unsubscribing from a private bug, because rendering the
136+ # bug page would raise Unauthorized errors!
137+ if self._redirecting_to_bug_list:
138+ return u''
139+ elif self._isSubscriptionRequest() and self.request.get('next_url'):
140+ self.request.response.redirect(self.request.get('next_url'))
141+ return u''
142+ else:
143+ return LaunchpadView.render(self)
144+
145+ def handleSubscriptionRequest(self):
146+ """Subscribe or unsubscribe the user from the bug, if requested."""
147+ if not self._isSubscriptionRequest():
148+ return
149+
150+ subscription_person = self.subscription_widget.getInputValue()
151+
152+ # 'subscribe' appears in the request whether the request is to
153+ # subscribe or unsubscribe. Since "subscribe someone else" is
154+ # handled by a different view we can assume that 'subscribe' +
155+ # current user as a parameter means "subscribe the current
156+ # user", and any other kind of 'subscribe' request actually
157+ # means "unsubscribe". (Yes, this *is* very confusing!)
158+ if ('subscribe' in self.request.form and
159+ (subscription_person == self.user)):
160+ self._handleSubscribe()
161+ else:
162+ self._handleUnsubscribe(subscription_person)
163+
164+ def _isSubscriptionRequest(self):
165+ """Return True if the form contains subscribe/unsubscribe input."""
166+ return (
167+ self.user and
168+ self.request.method == 'POST' and
169+ 'cancel' not in self.request.form and
170+ self.subscription_widget.hasValidInput())
171+
172+ def _handleSubscribe(self):
173+ """Handle a subscribe request."""
174+ self.context.bug.subscribe(self.user, self.user)
175+ self.request.response.addNotification(
176+ "You have been subscribed to this bug.")
177+
178+ def _handleUnsubscribe(self, user):
179+ """Handle an unsubscribe request."""
180+ if user == self.user:
181+ self._handleUnsubscribeCurrentUser()
182+ else:
183+ self._handleUnsubscribeOtherUser(user)
184+
185+ def _handleUnsubscribeCurrentUser(self):
186+ """Handle the special cases for unsubscribing the current user.
187+
188+ when the bug is private. The user must be unsubscribed from all dupes
189+ too, or they would keep getting mail about this bug!
190+ """
191+
192+ # ** Important ** We call unsubscribeFromDupes() before
193+ # unsubscribe(), because if the bug is private, the current user
194+ # will be prevented from calling methods on the main bug after
195+ # they unsubscribe from it!
196+ unsubed_dupes = self.context.bug.unsubscribeFromDupes(
197+ self.user, self.user)
198+ self.context.bug.unsubscribe(self.user, self.user)
199+
200+ self.request.response.addNotification(
201+ structured(
202+ self._getUnsubscribeNotification(self.user, unsubed_dupes)))
203+
204+ # Because the unsubscribe above may change what the security policy
205+ # says about the bug, we need to clear its cache.
206+ self.request.clearSecurityPolicyCache()
207+
208+ if not check_permission("launchpad.View", self.context.bug):
209+ # Redirect the user to the bug listing, because they can no
210+ # longer see a private bug from which they've unsubscribed.
211+ self.request.response.redirect(
212+ canonical_url(self.context.target) + "/+bugs")
213+ self._redirecting_to_bug_list = True
214+
215+ def _handleUnsubscribeOtherUser(self, user):
216+ """Handle unsubscribing someone other than the current user."""
217+ assert user != self.user, (
218+ "Expected a user other than the currently logged-in user.")
219+
220+ # We'll also unsubscribe the other user from dupes of this bug,
221+ # otherwise they'll keep getting this bug's mail.
222+ self.context.bug.unsubscribe(user, self.user)
223+ unsubed_dupes = self.context.bug.unsubscribeFromDupes(user, user)
224+ self.request.response.addNotification(
225+ structured(
226+ self._getUnsubscribeNotification(user, unsubed_dupes)))
227+
228+ def _getUnsubscribeNotification(self, user, unsubed_dupes):
229+ """Construct and return the unsubscribe-from-bug feedback message.
230+
231+ :user: The IPerson or ITeam that was unsubscribed from the bug.
232+ :unsubed_dupes: The list of IBugs that are dupes from which the
233+ user was unsubscribed.
234+ """
235+ current_bug = self.context.bug
236+ current_user = self.user
237+ unsubed_dupes_msg_fragment = self._getUnsubscribedDupesMsgFragment(
238+ unsubed_dupes)
239+
240+ if user == current_user:
241+ # Consider that the current user may have been "locked out"
242+ # of a bug if they unsubscribed themselves from a private
243+ # bug!
244+ if check_permission("launchpad.View", current_bug):
245+ # The user still has permission to see this bug, so no
246+ # special-casing needed.
247+ return (
248+ "You have been unsubscribed from bug %d%s." % (
249+ current_bug.id, unsubed_dupes_msg_fragment))
250+ else:
251+ return (
252+ "You have been unsubscribed from bug %d%s. You no "
253+ "longer have access to this private bug.") % (
254+ current_bug.id, unsubed_dupes_msg_fragment)
255+ else:
256+ return "%s has been unsubscribed from bug %d%s." % (
257+ cgi.escape(user.displayname), current_bug.id,
258+ unsubed_dupes_msg_fragment)
259+
260+ def _getUnsubscribedDupesMsgFragment(self, unsubed_dupes):
261+ """Return the duplicates fragment of the unsubscription notification.
262+
263+ This piece lists the duplicates from which the user was
264+ unsubscribed.
265+ """
266+ if not unsubed_dupes:
267+ return ""
268+
269+ dupe_links = []
270+ for unsubed_dupe in unsubed_dupes:
271+ dupe_links.append(
272+ '<a href="%s" title="%s">#%d</a>' % (
273+ canonical_url(unsubed_dupe), unsubed_dupe.title,
274+ unsubed_dupe.id))
275+ dupe_links_string = ", ".join(dupe_links)
276+
277+ num_dupes = len(unsubed_dupes)
278+ if num_dupes > 1:
279+ plural_suffix = "s"
280+ else:
281+ plural_suffix = ""
282+
283+ return (
284+ " and %(num_dupes)d duplicate%(plural_suffix)s "
285+ "(%(dupe_links_string)s)") % ({
286+ 'num_dupes': num_dupes,
287+ 'plural_suffix': plural_suffix,
288+ 'dupe_links_string': dupe_links_string})
289+
290+
291 class BugPortletSubcribersContents(LaunchpadView, BugViewMixin):
292 """View for the contents for the subscribers portlet."""
293
294
295=== modified file 'lib/lp/bugs/browser/bugtask.py'
296--- lib/lp/bugs/browser/bugtask.py 2010-10-06 18:27:57 +0000
297+++ lib/lp/bugs/browser/bugtask.py 2010-10-07 15:32:53 +0000
298@@ -115,7 +115,6 @@
299 )
300 from zope.schema.vocabulary import (
301 getVocabularyRegistry,
302- SimpleTerm,
303 SimpleVocabulary,
304 )
305 from zope.security.interfaces import Unauthorized
306@@ -162,7 +161,6 @@
307 from canonical.launchpad.webapp.batching import TableBatchNavigator
308 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
309 from canonical.launchpad.webapp.interfaces import ILaunchBag
310-from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
311 from canonical.launchpad.webapp.menu import structured
312 from canonical.lazr.interfaces import IObjectPrivacy
313 from canonical.lazr.utils import smartquote
314@@ -694,39 +692,13 @@
315 bug, 'title', canonical_url(self.context, view_name='+edit'),
316 id="bug-title", title="Edit this summary")
317
318- if self.user is None:
319- return
320-
321- # Set up widgets in order to handle subscription requests.
322- subscription_terms = []
323- self_subscribed = False
324- for person in bug.getSubscribersForPerson(self.user):
325- if person.id == self.user.id:
326- subscription_terms.append(
327- SimpleTerm(
328- person, person.name,
329- 'Unsubscribe me from this bug'))
330- self_subscribed = True
331- else:
332- subscription_terms.append(
333- SimpleTerm(
334- person, person.name,
335- 'Unsubscribe <a href="%s">%s</a> from this bug' % (
336- canonical_url(person),
337- cgi.escape(person.displayname))))
338- if not self_subscribed:
339- subscription_terms.insert(0,
340- SimpleTerm(
341- self.user, self.user.name, 'Subscribe me to this bug'))
342- subscription_vocabulary = SimpleVocabulary(subscription_terms)
343- person_field = Choice(
344- __name__='subscription',
345- vocabulary=subscription_vocabulary, required=True)
346- self.subscription_widget = CustomWidgetFactory(RadioWidget)
347- setUpWidget(
348- self, 'subscription', person_field, IInputWidget, value=self.user)
349-
350- self.handleSubscriptionRequest()
351+ # XXX 2010-10-05 gmb bug=655597:
352+ # This line of code keeps the view's query count down,
353+ # possibly using witchcraft. It should be rewritten to be
354+ # useful or removed in favour of making other queries more
355+ # efficient.
356+ if self.user is not None:
357+ list(bug.getSubscribersForPerson(self.user))
358
359 def userIsSubscribed(self):
360 """Is the user subscribed to this bug?"""
361@@ -734,22 +706,6 @@
362 self.context.bug.isSubscribed(self.user) or
363 self.context.bug.isSubscribedToDupes(self.user))
364
365- def shouldShowUnsubscribeFromDupesWarning(self):
366- """Should we warn the user about unsubscribing and duplicates?
367-
368- The warning should tell the user that, when unsubscribing, they
369- will also be unsubscribed from dupes of this bug.
370- """
371- if self.userIsSubscribed():
372- return True
373-
374- bug = self.context.bug
375- for team in self.user.teams_participated_in:
376- if bug.isSubscribed(team) or bug.isSubscribedToDupes(team):
377- return True
378-
379- return False
380-
381 def render(self):
382 """Render the bug list if the user has permission to see the bug."""
383 # Prevent normal rendering when redirecting to the bug list
384@@ -757,155 +713,9 @@
385 # bug page would raise Unauthorized errors!
386 if self._redirecting_to_bug_list:
387 return u''
388- elif self._isSubscriptionRequest() and self.request.get('next_url'):
389- self.request.response.redirect(self.request.get('next_url'))
390- return u''
391 else:
392 return LaunchpadView.render(self)
393
394- def handleSubscriptionRequest(self):
395- """Subscribe or unsubscribe the user from the bug, if requested."""
396- if not self._isSubscriptionRequest():
397- return
398-
399- subscription_person = self.subscription_widget.getInputValue()
400-
401- # 'subscribe' appears in the request whether the request is to
402- # subscribe or unsubscribe. Since "subscribe someone else" is
403- # handled by a different view we can assume that 'subscribe' +
404- # current user as a parameter means "subscribe the current
405- # user", and any other kind of 'subscribe' request actually
406- # means "unsubscribe". (Yes, this *is* very confusing!)
407- if ('subscribe' in self.request.form and
408- (subscription_person == self.user)):
409- self._handleSubscribe()
410- else:
411- self._handleUnsubscribe(subscription_person)
412-
413- def _isSubscriptionRequest(self):
414- """Return True if the form contains subscribe/unsubscribe input."""
415- return (
416- self.user and
417- self.request.method == 'POST' and
418- 'cancel' not in self.request.form and
419- self.subscription_widget.hasValidInput())
420-
421- def _handleSubscribe(self):
422- """Handle a subscribe request."""
423- self.context.bug.subscribe(self.user, self.user)
424- self.request.response.addNotification("You have been subscribed to this bug.")
425-
426- def _handleUnsubscribe(self, user):
427- """Handle an unsubscribe request."""
428- if user == self.user:
429- self._handleUnsubscribeCurrentUser()
430- else:
431- self._handleUnsubscribeOtherUser(user)
432-
433- def _handleUnsubscribeCurrentUser(self):
434- """Handle the special cases for unsubscribing the current user.
435-
436- when the bug is private. The user must be unsubscribed from all dupes
437- too, or they would keep getting mail about this bug!
438- """
439-
440- # ** Important ** We call unsubscribeFromDupes() before
441- # unsubscribe(), because if the bug is private, the current user
442- # will be prevented from calling methods on the main bug after
443- # they unsubscribe from it!
444- unsubed_dupes = self.context.bug.unsubscribeFromDupes(
445- self.user, self.user)
446- self.context.bug.unsubscribe(self.user, self.user)
447-
448- self.request.response.addNotification(
449- structured(
450- self._getUnsubscribeNotification(self.user, unsubed_dupes)))
451-
452- # Because the unsubscribe above may change what the security policy
453- # says about the bug, we need to clear its cache.
454- self.request.clearSecurityPolicyCache()
455-
456- if not check_permission("launchpad.View", self.context.bug):
457- # Redirect the user to the bug listing, because they can no
458- # longer see a private bug from which they've unsubscribed.
459- self.request.response.redirect(
460- canonical_url(self.context.target) + "/+bugs")
461- self._redirecting_to_bug_list = True
462-
463- def _handleUnsubscribeOtherUser(self, user):
464- """Handle unsubscribing someone other than the current user."""
465- assert user != self.user, (
466- "Expected a user other than the currently logged-in user.")
467-
468- # We'll also unsubscribe the other user from dupes of this bug,
469- # otherwise they'll keep getting this bug's mail.
470- self.context.bug.unsubscribe(user, self.user)
471- unsubed_dupes = self.context.bug.unsubscribeFromDupes(user, user)
472- self.request.response.addNotification(
473- structured(
474- self._getUnsubscribeNotification(user, unsubed_dupes)))
475-
476- def _getUnsubscribeNotification(self, user, unsubed_dupes):
477- """Construct and return the unsubscribe-from-bug feedback message.
478-
479- :user: The IPerson or ITeam that was unsubscribed from the bug.
480- :unsubed_dupes: The list of IBugs that are dupes from which the
481- user was unsubscribed.
482- """
483- current_bug = self.context.bug
484- current_user = self.user
485- unsubed_dupes_msg_fragment = self._getUnsubscribedDupesMsgFragment(
486- unsubed_dupes)
487-
488- if user == current_user:
489- # Consider that the current user may have been "locked out"
490- # of a bug if they unsubscribed themselves from a private
491- # bug!
492- if check_permission("launchpad.View", current_bug):
493- # The user still has permission to see this bug, so no
494- # special-casing needed.
495- return (
496- "You have been unsubscribed from bug %d%s." % (
497- current_bug.id, unsubed_dupes_msg_fragment))
498- else:
499- return (
500- "You have been unsubscribed from bug %d%s. You no "
501- "longer have access to this private bug.") % (
502- current_bug.id, unsubed_dupes_msg_fragment)
503- else:
504- return "%s has been unsubscribed from this bug%s." % (
505- cgi.escape(user.displayname), unsubed_dupes_msg_fragment)
506-
507- def _getUnsubscribedDupesMsgFragment(self, unsubed_dupes):
508- """Return the duplicates fragment of the unsubscription notification.
509-
510- This piece lists the duplicates from which the user was
511- unsubscribed.
512- """
513- if not unsubed_dupes:
514- return ""
515-
516- dupe_links = []
517- for unsubed_dupe in unsubed_dupes:
518- dupe_links.append(
519- '<a href="%s" title="%s">#%d</a>' % (
520- canonical_url(unsubed_dupe), unsubed_dupe.title,
521- unsubed_dupe.id))
522- dupe_links_string = ", ".join(dupe_links)
523-
524- num_dupes = len(unsubed_dupes)
525- if num_dupes > 1:
526- plural_suffix = "s"
527- else:
528- plural_suffix = ""
529-
530- return (
531- " and %(num_dupes)d duplicate%(plural_suffix)s "
532- "(%(dupe_links_string)s)") % ({
533- 'num_dupes': num_dupes,
534- 'plural_suffix': plural_suffix,
535- 'dupe_links_string': dupe_links_string})
536-
537 def _nominateBug(self, series):
538 """Nominate the bug for the series and redirect to the bug page."""
539 self.context.bug.addNomination(self.user, series)
540
541=== modified file 'lib/lp/bugs/browser/configure.zcml'
542--- lib/lp/bugs/browser/configure.zcml 2010-10-03 15:30:06 +0000
543+++ lib/lp/bugs/browser/configure.zcml 2010-10-07 15:32:53 +0000
544@@ -725,7 +725,7 @@
545 <browser:page
546 for="lp.bugs.interfaces.bugtask.IBugTask"
547 name="+subscribe"
548- class="lp.bugs.browser.bugtask.BugTaskView"
549+ class="lp.bugs.browser.bugsubscription.BugSubscriptionSubscribeSelfView"
550 permission="launchpad.AnyPerson"
551 template="../templates/bug-subscription.pt"/>
552 <browser:page
553
554=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt'
555--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2010-10-04 14:20:00 +0000
556+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2010-10-07 15:32:53 +0000
557@@ -124,7 +124,7 @@
558
559 >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
560 ... print tag.renderContents()
561- Launchpad Developers has been unsubscribed from this bug.
562+ Launchpad Developers has been unsubscribed from bug 1.
563
564 >>> browser.open(
565 ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
566@@ -337,7 +337,7 @@
567 >>> for tag in find_tags_by_class(
568 ... foobar_browser.contents, 'informational message'):
569 ... print tag.renderContents()
570- Ubuntu Team has been unsubscribed from this bug and 1 duplicate (<a...#2</a>)...
571+ Ubuntu Team has been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
572
573 (ubuntu-team is no longer an indirect subscriber.)
574
575
576=== modified file 'lib/lp/bugs/templates/bug-subscription.pt'
577--- lib/lp/bugs/templates/bug-subscription.pt 2010-09-29 20:33:19 +0000
578+++ lib/lp/bugs/templates/bug-subscription.pt 2010-10-07 15:32:53 +0000
579@@ -34,7 +34,7 @@
580 duplicates.
581 </p>
582
583- <form action="." method="POST">
584+ <form action="" method="POST">
585
586 <div class="field">
587 <div>
588
589=== modified file 'lib/lp/registry/browser/person.py'
590--- lib/lp/registry/browser/person.py 2010-10-04 20:46:55 +0000
591+++ lib/lp/registry/browser/person.py 2010-10-07 15:32:53 +0000
592@@ -2368,7 +2368,13 @@
593
594 for task in bug_tasks:
595 if task.bug not in sub_bugs:
596- sub_bug_tasks.append(task)
597+ # We order the bugtasks by date_last_updated but we
598+ # always display the default task for the bug. This is
599+ # to avoid ordering issues in tests and also prevents
600+ # user confusion (because nothing is more confusing than
601+ # your subscription targets changing seemingly at
602+ # random).
603+ sub_bug_tasks.append(task.bug.default_bugtask)
604 sub_bugs.append(task.bug)
605 return BatchNavigator(sub_bug_tasks, self.request)
606
607
608=== modified file 'lib/lp/registry/stories/person/xx-person-subscriptions.txt'
609--- lib/lp/registry/stories/person/xx-person-subscriptions.txt 2010-10-05 16:17:07 +0000
610+++ lib/lp/registry/stories/person/xx-person-subscriptions.txt 2010-10-07 15:32:53 +0000
611@@ -110,7 +110,7 @@
612 In
613 1
614 Firefox does not support SVG
615- mozilla-firefox (Ubuntu)
616+ Mozilla Firefox
617
618 Sample Person now chooses to unsubscribe HWDB team from bug 1.
619