Merge lp:~brian-murray/launchpad/limited-subscriptions-page into lp:launchpad

Proposed by Brian Murray
Status: Merged
Approved by: Brian Murray
Approved revision: no longer in the source branch.
Merged at revision: 11669
Proposed branch: lp:~brian-murray/launchpad/limited-subscriptions-page
Merge into: lp:launchpad
Diff against target: 587 lines (+343/-20)
11 files modified
lib/canonical/launchpad/webapp/launchpadform.py (+1/-1)
lib/lp/bugs/browser/bugtask.py (+35/-4)
lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt (+1/-1)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt (+9/-8)
lib/lp/bugs/templates/bug-subscription.pt (+5/-2)
lib/lp/registry/browser/configure.zcml (+6/-0)
lib/lp/registry/browser/person.py (+49/-1)
lib/lp/registry/browser/tests/test_person_view.py (+32/-2)
lib/lp/registry/stories/person/xx-person-subscriptions.txt (+131/-0)
lib/lp/registry/stories/productseries/xx-productseries-delete.txt (+1/-1)
lib/lp/registry/templates/person-subscriptions.pt (+73/-0)
To merge this branch: bzr merge lp:~brian-murray/launchpad/limited-subscriptions-page
Reviewer Review Type Date Requested Status
Curtis Hovey (community) ui Approve
Guilherme Salgado (community) ui* Approve
Graham Binns (community) code Approve
Review via email: mp+35177@code.launchpad.net

Commit message

Add in the start of a +subscriptions page for managing subscriptions in Launchpad. Currently contains direct bug subscriptions.

Description of the change

= Summary =

This branch is the start of a +subscriptions page for people and teams that will display information about all items that people are subscribed to in Launchpad and allow them to unsubscribe from those items.

As this is the first revision only direct bug subscriptions have been included and the page is not yet linked to.

== Pre-implementation notes ==

Deryck and I talked about this and decided that it would be worthwhile to show duplicate bugs as these can be harder for people to unsubscribe from these. I've also decided to include Invalid as these can receive noisy traffic.

== Implementation details ==

lib/lp/registry/browser/configure.zcml
    add in +subscriptions page

lib/lp/registry/browser/person.py
    add in PersonSubscriptionsView class
    subscribedBugTasks() returns only one task per bug to prevent duplicates showing up as you can only unsubscribe from bugs not bug tasks
    fix a typo in getSimpleSearchURL()

lib/lp/registry/stories/person/xx-person-subscriptions.txt
    created a new test for the +subscriptions page

lib/lp/registry/templates/person-subscriptions.pt
    created the +subscriptions page

== Tests ==

bin/test -cvvt xx-person-subscriptions.txt

== Demo and QA ==

Login in as sample person and go to http://launchpad.dev/~name12/+subscriptions also check http://launchpad.dev/~ubuntu-team/+subscriptions for a person with no bug subscriptions.

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

Hi Brian,

I like this branch; nice work. Couple of minor changes needed, but
nothing that isn't just nitpicking. I've also posed a question about
performance, but I'm not sure that we can do much about at this point.

You should get a UI review for this branch from one of our sainted UI
reviewers before you land it.

> === modified file 'lib/lp/registry/browser/person.py'
> --- lib/lp/registry/browser/person.py 2010-09-10 12:24:03 +0000
> +++ lib/lp/registry/browser/person.py 2010-09-10 22:30:12 +0000
> @@ -2396,6 +2397,42 @@
> return self.getSearchPageHeading()
>
>
> +class PersonSubscriptionsView(BugTaskSearchListingView):
> + """All the subscriptions for a person."""
> +
> + page_title = 'Subscriptions'
> +
> + def subscribedBugTasks(self):
> + """Return a BatchNavigator for distinct bug tasks to which the
> + person is subscribed."""
> + bugtasks = self.context.searchTasks(None, user=self.user,

bugtasks => bug_tasks.

> + order_by='-date_last_updated',
> + status=(BugTaskStatus.NEW,
> + BugTaskStatus.INCOMPLETE,
> + BugTaskStatus.CONFIRMED,
> + BugTaskStatus.TRIAGED,
> + BugTaskStatus.INPROGRESS,
> + BugTaskStatus.FIXCOMMITTED,
> + BugTaskStatus.FIXRELEASED))

You can outdent all of this by four spaces; we use four-space indents
per level in method calls.

> +
> + sub_bugtasks = []
> + sub_bugs = []
> +
> + for task in bugtasks:
> + if task.bug not in sub_bugs:
> + sub_bugtasks.append(task)
> + sub_bugs.append(task.bug)

Is this loop going to cause performance problems for people with a large
number of direct subscriptions?

> + return BatchNavigator(sub_bugtasks, self.request)
> +
> + def getSubscriptionsPageHeading(self):
> + """The header for the subscriptions page."""
> + return "Subscriptions for %s" % self.context.displayname
> +
> + @property
> + def label(self):
> + return self.getSubscriptionsPageHeading()
> +
> +
> class PersonVouchersView(LaunchpadFormView):
> """Form for displaying and redeeming commercial subscription vouchers."""
>
> === added file 'lib/lp/registry/stories/person/xx-person-subscriptions.txt'
> --- lib/lp/registry/stories/person/xx-person-subscriptions.txt 1970-01-01 00:00:00 +0000
> +++ lib/lp/registry/stories/person/xx-person-subscriptions.txt 2010-09-10 22:30:12 +0000
> @@ -0,0 +1,56 @@
> +Subscriptions View
> +==================
> +
> +Direct bug subscriptions
> +
> +Each person and team has a subscriptions page that lists things to which they
> +are directly subscribed.
> +
> + >>> anon_browser.open('http://launchpad.dev/~ubuntu-team/+subscriptions')
> + >>> print anon_browser.title
> + Subscriptions : “Ubuntu Team” team
> +
> +The bug subscriptions table does not appear for people without any direct bug
> +subscriptions.
> +
> + >>> page_text = extract_text(find_main_content(anon_browser.contents))
> + >>> "does not have any direct bug subscriptions" in page_text
> + Tru...

Read more...

review: Approve (code)
Revision history for this message
Guilherme Salgado (salgado) wrote :

Hi Brian,

It's great to see that work is being done on this long-awaited feature, but I was expecting to see some mockups for what we expect this page to look like and how it'd work. Something similar to what the Soyuz team is doing with derivative distributions. Do you know if there's a LEP for this? Or maybe just some mockups? I think it's important to work on those (maybe with help from the Design team, as described in https://dev.launchpad.net/UI/Design) for this feature, as the current approach may not scale well once we start adding the other types of items that a person can subscribe to this page.

Regardless of that, here's some feedback on the current implementation.

In the long term I'm sure we want to use ajax to unsubscribe without leaving the page, and given how easy it is not much more work to do so, I think it'd make sense to do that from the start. That would probably make an undo functionality more important, but I think we can live without that for now as that'd complicate things a bit more.

Also, upon clicking the unsubscribe link, you're taken to the bug's +subscribe page, and clicking Cancel there will take you to the bug's +index page rather than to +subscriptions as one would expect. This wouldn't be a problem if we were using ajax.

The page should not include the unsubscribe buttons when looking at somebody else's page, as the only thing you can do is unsubscribe yourself but not the person whose page you're looking at. Either that or make the page not available for other users.

For teams, the unsubscribe link takes you to the bug's +subscribe page but if you're not currently subscribed to that bug, the default option (and the heading) is for you to subscribe yourself rather than to unsubscribe the team. When you are subscribed, the default options is to unsubscribe yourself. This is a bit confusing, but we could avoid this completely by using ajax to unsubscribe directly from +subscriptions.

review: Abstain (ui*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

I think Salgado is review of the UI is very good. This is a much needed feature, but it introduces a lot of UI confusion. This feature needs a feature flag to ensure lpnet users do not see this until it is ready.

This UI will get complicated in the next year because we do need to move blueprint and answers subscriptions into this page. I think AJAX is going to be a requirement by then.

I do not think this can land until some of the obvious UI confusion is addressed.

You can use this mixin so that the view returns the user to the correct page:
    from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
But I think switching to AJAX will be the only way to address the clarity of the operation that the user needs to accomplish--users can be in 50 teams and that will distract the user from his task.

Do not show me remove icon when the page is an edit operations (switching to AJAX will fix this).

Do not show me edit operations for other users and teams I am not a member of.

The story is not a story. It is not written from the perspective of a user who visits Lp to accomplish a task. I expect to see the user arrive at his profile page, choose the link to +subscriptions, then use a remove link to remove the subscription, then he sees the bug is not listed. I suppose we want a similar story for removing a subscription for a user's team. The conditions for not showing the bug listing belong in a test for the view

I have read the story and I have no idea how I will discover the link to this page. I assume we are not publishing the link in pages to prevent users from seeing the link until we are ready. If this is the case you need an XXX in the story explaining why the user does not start on the page that directs the user to look at his subscriptions. The other course is to use a feature flag that hides the link and the page until we are ready.

review: Needs Fixing (ui)
Revision history for this message
Brian Murray (brian-murray) wrote :

This is the first branch I've done requiring UI review or that is fulfilling a LEP. The LEP is located at - https://dev.launchpad.net/Research/Bugs/Subscriptions2010.

Revision history for this message
Guilherme Salgado (salgado) wrote :

That looks more like a report from user testing sessions than a LEP. I
was expecting something that would clearly say how the UI should
look/behave. Is that the only document you're using to drive the
implementation of this? Maybe it'd be worth writing a LEP first?

Revision history for this message
Deryck Hodge (deryck) wrote :
Download full text (4.2 KiB)

Hi, salgado and sinzui.

We do have a LEP for this feature. This +subscriptions page is one bit of the "Better Subscriptions and Notifications" story we're currently doing development work on. The LEP is at:

https://dev.launchpad.net/LEP/BetterBugSubscriptionsAndNotifications

This is an old LEP now, and I think we've refined the process a bit since writing this one, so maybe some elements are missing that should be there. This is not Brian's fault, so please don't block Brian for this reason. Let me know if you need something there, and I'll add it.

The link Brian provided is the result of the user research we did with mrevell for all of the UI elements within this story.

https://dev.launchpad.net/Research/Bugs/Subscriptions2010

A bit of history here may help with some of your concerns, too. We did try to work with the DUX team on this feature. Francis, Jono, Tom, Graham, and I did a fair amount of coordination work, and in the end we spent two months chasing meetings, which slowed the progress of this feature. We did get valuable feedback from a couple sessions with Alejandra about subscriptions UI elements, and this +subscriptions page is a direct result of that feedback. We also then did two rounds of user testing on these mockups with mrevell. I think we've more than covered ourselves in terms of thinking about the UI before beginning to implement it.

Having said that, I don't think https://dev.launchpad.net/UI/Design should be consider the gospel for UI procedure, at least the initial pre-imp recommendation in its current form. We on the bugs team in particular are trying to experiment with approaches to UI design to achieve quicker turnaround on both producing mockups and receiving feedback. I think a pre-imp with a UI reviewer should be considered the same as for a code reviewer -- recommended, but the designer/programmer can use his/her own discretion. We also cannot block on speaking with the DUX team as my team has already proven this will delay work beyond a reasonable amount of time. Mockups, of course, are a reasonable requirement. Regardless, in this case, we've covered all our bases. ;)

Finally, as to some of your more technical concerns. I have asked Brian to land this page without any Ajax enabled widgets initially. The subscription widget is not easily re-usable, and it would require one or two more branches to get widgets enabled for this page. This is meant to be the very first step in this process for Brian. He has many more branches to go until this work is completed -- adding Ajax widgets, adding sections for every item we support a subscription on, good paginating for these things, performance tuning every step of the way, and finally making a link available to users so they become aware of this page. I would prefer we don't add a link and hide it with a feature flag since you guys are correct that we haven't yet thought about where this link should go. I think there is value in telling a few key stakeholders about this page as we work on it, and this seems simpler to me than feature flagging it, at least at this point in the life of the feature flag system. If we do require this, let's have...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (5.3 KiB)

On Mon, 2010-09-20 at 13:52 +0000, Deryck Hodge wrote:
> Hi, salgado and sinzui.
>
> We do have a LEP for this feature. This +subscriptions page is one
> bit of the "Better Subscriptions and Notifications" story we're
> currently doing development work on. The LEP is at:
>
> https://dev.launchpad.net/LEP/BetterBugSubscriptionsAndNotifications
>
> This is an old LEP now, and I think we've refined the process a bit
> since writing this one, so maybe some elements are missing that should
> be there. This is not Brian's fault, so please don't block Brian for
> this reason. Let me know if you need something there, and I'll add
> it.
>
> The link Brian provided is the result of the user research we did with
> mrevell for all of the UI elements within this story.
>
> https://dev.launchpad.net/Research/Bugs/Subscriptions2010
>
> A bit of history here may help with some of your concerns, too. We
> did try to work with the DUX team on this feature. Francis, Jono,
> Tom, Graham, and I did a fair amount of coordination work, and in the
> end we spent two months chasing meetings, which slowed the progress of
> this feature. We did get valuable feedback from a couple sessions
> with Alejandra about subscriptions UI elements, and this
> +subscriptions page is a direct result of that feedback. We also then
> did two rounds of user testing on these mockups with mrevell. I think
> we've more than covered ourselves in terms of thinking about the UI
> before beginning to implement it.

Cool, that's good to know. I asked for a LEP because I'd expect it to
either contain some of this information or at show me that this UI (and
possibly others) had been thought through. By looking at the merge
proposal this was not clear.

>
> Having said that, I don't think https://dev.launchpad.net/UI/Design
> should be consider the gospel for UI procedure, at least the initial
> pre-imp recommendation in its current form. We on the bugs team in
> particular are trying to experiment with approaches to UI design to
> achieve quicker turnaround on both producing mockups and receiving
> feedback. I think a pre-imp with a UI reviewer should be considered
> the same as for a code reviewer -- recommended, but the
> designer/programmer can use his/her own discretion. We also cannot
> block on speaking with the DUX team as my team has already proven this
> will delay work beyond a reasonable amount of time. Mockups, of
> course, are a reasonable requirement. Regardless, in this case, we've
> covered all our bases. ;)

I completely agree.

Someone (I think Curtis) said that UI reviews are more about making sure
people think carefully about UI changes rather than anything else, and
that's what I was trying to do by pointing to the wiki page. Again, the
merge proposal had no indication any of this was done.

>
> Finally, as to some of your more technical concerns. I have asked
> Brian to land this page without any Ajax enabled widgets initially.
> The subscription widget is not easily re-usable, and it would require
> one or two more branches to get widgets enabled for this page. This
> is meant to be the very first step in this process for Brian. He has
> many ...

Read more...

review: Needs Fixing
Revision history for this message
Robert Collins (lifeless) wrote :

+1 on not exposing this until we're fairly comfortable with it.

You can enable it via a feature flag to permit user testing as we go:

<a tal:condition="features/bugs.subscriptions_page" href="link to the
page here" />

Revision history for this message
Curtis Hovey (sinzui) wrote :

I am review r11483,

I am seeing some problems as Sample Person when I try to manage my subscriptions
    https://launchpad.dev/~name12/+subscriptions
 * I choose to remove the blackhole subscription. The link to edit it shows a radio button. I cannot unselected it. I can change it.
 * If The cancel link goes to the bug, not +subscriptions

Check my teams (as Sample Person)
    https://launchpad.dev/~hwdb-team/+subscriptions
 * I cannot unselect the radio button for the team. I can of course select myself to deselect the team, but I do not want to subscribe to the bug
 * Again the cancel link the bug, not +subscriptions

As anonymous when I see https://launchpad.dev/~hwdb-team/+subscriptions I read
    HWDB Team does not have any direct bug subscriptions.
^ that is great, I think the text for the bug listing table should make it clear I am seeing "direct bug subscriptions"

I think part of the problem here is that I am reading the story, but it is clearly not a story. A story is from a users perspective and explains his experience performing a task. There are not tasks completed in this story, and when I try to complete what I think Sample Person is doing, I fail. I will reply in about 30 minutes with a revised story I what I think the users' goals are.

review: Needs Fixing (ui)
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (5.6 KiB)

This is closer to a proper story and described the goals that Sample Person uses
+subscriptions to accomplish.

Subscriptions View
==================

XXX: bdmurray 2010-09-24 bug=628411 This story is complete until we publish
the link that leads to the +subscriptions page.

XXX: A story is written from a user's perspective. It explains how a user
accomplishes a task and how he know it. Use user names and rules when
telling the story. Stories do not test failure,
permissions, or method functions--that is the domain of unittests.

Any user can view the direct subcriptions that a person or team has to
bugs, blueprints, questions, branches, and merge proposals.

    >>> anon_browser.open('http://launchpad.dev/~ubuntu-team/+subscriptions')
    >>> print anon_browser.title
    Subscriptions : “Ubuntu Team” team

The user can see that the Ubuntu Team does not have an direct bug
subscritions.

    >>> page_text = extract_text(find_main_content(anon_browser.contents))
    >>> "does not have any direct bug subscriptions" in page_text
    True

Any user can see that that Sample Person is subscribed to several bugs.
The bug subscriptions table includes the bug number, title and location.

    >>> anon_browser.open('http://launchpad.dev/~name12/+subscriptions')
    >>> bug_sub_table = find_tag_by_id(
    ... anon_browser.contents, 'bug_subscriptions')
    >>> for tr in bug_sub_table.findAll('tr'):
    ... print extract_text(tr)
    Summary
    In
    13
    Launchpad CSS and JS is not testible
    Launchpad
    5
    Firefox install instructions should be complete
    ...
    4
    Reflow problems with complex page layouts
    Mozilla Firefox
    2
    Blackhole Trash folder
    ...
    9
    Thunderbird crashes
    thunderbird (Ubuntu)

Sample Person can see his see his direct bug subscriptions too, and he can
use the page manage his direct subscriptions. He chooses to view
bug 13, "Launchpad CSS and JS is not testible".

    >>> sample_browser = setupBrowser(auth='Basic <email address hidden>:test')
    >>> sample_browser.open('http://bugs.launchpad.dev/~name12/+subscriptions')
    >>> sample_browser.getLink(id='unsubscribe-subscriber-12').click()
    >>> print sample_browser.title
   Unsubscribe from bug #13

After viewing the +subscribe page Sample Person decides that he want to
reamain subscribed to bug 13. He uses the cancel link to

    >>> sample_browser.getLink('Cancel').click()
    >>> print sample_browser.title
    Subscriptions: Sample Person

He chooses to unsubscribe from bug 2, "Blackhole Trash folder".

    >>> sample_browser.getLink(id='unsubscribe-subscriber-2').click()
    >>> print sample_browser.title
   Unsubscribe from bug #2

Sample Person unselects the checkbox and continues.

    >>> sample_browser.getControl('Unsubscribe me from this bug') = False
    >>> sample_browser.getControl('Continue').click()
    >>> print sample_browser.title
    Subscriptions: Sample Person

Sample Person can see that bug #2 is not listed in his direct bug
subscriptions.

    >>> print extract_text(find_tag_by_id(
    ... sample_browser.contents, 'bug_subscriptions'))
    Summary In
    13 Launchp...

Read more...

Revision history for this message
Brian Murray (brian-murray) wrote :

I believe I've addressed all the issues with the review of this branch and feel it is now ready for review again.

Revision history for this message
Guilherme Salgado (salgado) wrote :

Thanks for fixing the things I'd reported, Brian. The only remaining issue I see is the 'Cancel' link on +subscribe which still sends me to the bug's page, but that might be a bug on ReturnToReferrerMixin or just an oversight on your side. If it's the former, it's fine to just file a bug, but if it's the latter it should be trivial to fix.

review: Approve (ui*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Salgado, Brian BugTaskView's cancel url is not hooked up the same way that next_url is. I think this is a simple property, or it may be cancel_url = next_url in the view.

Thanks for making the changes Brian. I think this is good to land.

review: Approve (ui)
Revision history for this message
Guilherme Salgado (salgado) wrote :

The Continue button shows the same behaviour for me, but what is weird is that they work as we expect in the test.

I couldn't figure out what is going on, but this might be related to the fact that ReturnToReferrerMixin must be mixed into a LaunchpadFormView, but here it was mixed into a LaunchpadView. That mixin works by storing a hidden field (_return_url) in the page's form and mapping that field to a view property with the same name. Here the hidden field is not stored automatically by the mixin as the page does not use launchpad-form.pt (since it's not a LaunchpadFormView), so the hidden field is created manually. The hidden field created manually has a different name, though ('next_url'), so the mixin's _return_url property would always return None. That's consistent with the behaviour I see on the two browsers I've tested (chromium and epiphany), but is not consistent with the behaviour shown by the tests.

Revision history for this message
Guilherme Salgado (salgado) wrote :

There's one thing which I overlooked in my analysis above: ReturnToReferrerMixin._return_url will return self.request.getHeader('referer') when a _return_url is not present in the request. So there must be something that is stripping referrers out of my requests. I'll figure it out; sorry for the false alarm.

Revision history for this message
Guilherme Salgado (salgado) wrote :

Damn it, found what's going on!

The test passes because it accesses the +subscription page on the bugs vhost. It works for me if I use that vhost as well.

If you use the main vhost, it won't work. I think this is a problem because this page will mix content from multiple apps, so it should probably be accessible on the main vhost.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
--- lib/canonical/launchpad/webapp/launchpadform.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/launchpadform.py 2010-09-30 22:08:08 +0000
@@ -460,7 +460,7 @@
460 between the request to view the form and submitting the form.460 between the request to view the form and submitting the form.
461461
462 _return_attribute_name and _return_attribute_values are also stored462 _return_attribute_name and _return_attribute_values are also stored
463 as hidden fields and they are use to check the validity of _return_url.463 as hidden fields and they are used to check the validity of _return_url.
464464
465 If _return_url depends on _return_attribute_name, the result of a form465 If _return_url depends on _return_attribute_name, the result of a form
466 submission can invalidate it.466 submission can invalidate it.
467467
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-09-28 14:50:19 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-09-30 22:08:08 +0000
@@ -162,6 +162,7 @@
162from canonical.launchpad.webapp.batching import TableBatchNavigator162from canonical.launchpad.webapp.batching import TableBatchNavigator
163from canonical.launchpad.webapp.breadcrumb import Breadcrumb163from canonical.launchpad.webapp.breadcrumb import Breadcrumb
164from canonical.launchpad.webapp.interfaces import ILaunchBag164from canonical.launchpad.webapp.interfaces import ILaunchBag
165from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
165from canonical.launchpad.webapp.menu import structured166from canonical.launchpad.webapp.menu import structured
166from lp.app.browser.tales import (167from lp.app.browser.tales import (
167 FormattersAPI,168 FormattersAPI,
@@ -620,7 +621,8 @@
620 return view.render()621 return view.render()
621622
622623
623class BugTaskView(LaunchpadView, BugViewMixin, CanBeMentoredView, FeedsMixin):624class BugTaskView(LaunchpadView, BugViewMixin, CanBeMentoredView,
625 FeedsMixin):
624 """View class for presenting information about an `IBugTask`."""626 """View class for presenting information about an `IBugTask`."""
625627
626 override_title_breadcrumbs = True628 override_title_breadcrumbs = True
@@ -647,6 +649,32 @@
647 bugtask.bug.id, bugtask.bugtargetdisplayname)649 bugtask.bug.id, bugtask.bugtargetdisplayname)
648 return smartquote('%s: "%s"') % (heading, self.context.bug.title)650 return smartquote('%s: "%s"') % (heading, self.context.bug.title)
649651
652 @property
653 def next_url(self):
654 """Provided so returning to the page they came from works."""
655 referer = self.request.getHeader('referer')
656
657 # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
658 # browser setting referer to localhost.
659 if referer and referer != 'localhost':
660 next_url = referer
661 else:
662 next_url = canonical_url(self.context)
663 return next_url
664
665 @property
666 def cancel_url(self):
667 """Provided so returning to the page they came from works."""
668 referer = self.request.getHeader('referer')
669
670 # XXX bdmurray 2010-09-30 bug=98437: work around zope's test
671 # browser setting referer to localhost.
672 if referer and referer != 'localhost':
673 cancel_url = referer
674 else:
675 cancel_url = canonical_url(self.context)
676 return cancel_url
677
650 def initialize(self):678 def initialize(self):
651 """Set up the needed widgets."""679 """Set up the needed widgets."""
652 bug = self.context.bug680 bug = self.context.bug
@@ -728,6 +756,9 @@
728 # bug page would raise Unauthorized errors!756 # bug page would raise Unauthorized errors!
729 if self._redirecting_to_bug_list:757 if self._redirecting_to_bug_list:
730 return u''758 return u''
759 elif self._isSubscriptionRequest() and self.request.get('next_url'):
760 self.request.response.redirect(self.request.get('next_url'))
761 return u''
731 else:762 else:
732 return LaunchpadView.render(self)763 return LaunchpadView.render(self)
733764
@@ -761,7 +792,7 @@
761 def _handleSubscribe(self):792 def _handleSubscribe(self):
762 """Handle a subscribe request."""793 """Handle a subscribe request."""
763 self.context.bug.subscribe(self.user, self.user)794 self.context.bug.subscribe(self.user, self.user)
764 self.notices.append("You have been subscribed to this bug.")795 self.request.response.addNotification("You have been subscribed to this bug.")
765796
766 def _handleUnsubscribe(self, user):797 def _handleUnsubscribe(self, user):
767 """Handle an unsubscribe request."""798 """Handle an unsubscribe request."""
@@ -833,8 +864,8 @@
833 # The user still has permission to see this bug, so no864 # The user still has permission to see this bug, so no
834 # special-casing needed.865 # special-casing needed.
835 return (866 return (
836 "You have been unsubscribed from this bug%s." %867 "You have been unsubscribed from bug %d%s." % (
837 unsubed_dupes_msg_fragment)868 current_bug.id, unsubed_dupes_msg_fragment))
838 else:869 else:
839 return (870 return (
840 "You have been unsubscribed from bug %d%s. You no "871 "You have been unsubscribed from bug %d%s. You no "
841872
=== modified file 'lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt'
--- lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2010-08-22 18:31:30 +0000
+++ lib/lp/bugs/stories/bug-privacy/40-unsubscribe-from-private-bug.txt 2010-09-30 22:08:08 +0000
@@ -58,7 +58,7 @@
58 >>> browser.getControl("Continue").click()58 >>> browser.getControl("Continue").click()
5959
60 >>> browser.url60 >>> browser.url
61 'http://launchpad.dev/ubuntu/+source/evolution/+bug/...'61 'http://bugs.launchpad.dev/ubuntu/+source/evolution/+bug/...'
62 >>> "You have been unsubscribed" in browser.contents62 >>> "You have been unsubscribed" in browser.contents
63 True63 True
6464
6565
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2010-09-09 21:00:54 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2010-09-30 22:08:08 +0000
@@ -28,7 +28,7 @@
28 >>> submit.click()28 >>> submit.click()
2929
30 >>> browser.url30 >>> browser.url
31 'http://bugs.launchpad.dev/firefox/+bug/1/'31 'http://bugs.launchpad.dev/firefox/+bug/1'
3232
33 >>> for tag in find_tags_by_class(browser.contents, "informational message"):33 >>> for tag in find_tags_by_class(browser.contents, "informational message"):
34 ... print tag.renderContents()34 ... print tag.renderContents()
@@ -72,7 +72,7 @@
7272
73 >>> for tag in find_tags_by_class(browser.contents, 'informational message'):73 >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
74 ... print tag.renderContents()74 ... print tag.renderContents()
75 You have been unsubscribed from this bug.75 You have been unsubscribed from bug 1.
7676
77Users can unsubscribe teams to which they belong. Let's demonstrate by77Users can unsubscribe teams to which they belong. Let's demonstrate by
78first subscribing one of Foo Bar's teams.78first subscribing one of Foo Bar's teams.
@@ -120,7 +120,7 @@
120 >>> browser.getControl('Continue').click()120 >>> browser.getControl('Continue').click()
121121
122 >>> browser.url122 >>> browser.url
123 'http://bugs.launchpad.dev/firefox/+bug/1/'123 'http://bugs.launchpad.dev/firefox/+bug/1'
124124
125 >>> for tag in find_tags_by_class(browser.contents, 'informational message'):125 >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
126 ... print tag.renderContents()126 ... print tag.renderContents()
@@ -146,7 +146,7 @@
146 ['name16']146 ['name16']
147 >>> browser.getLink('Cancel').click()147 >>> browser.getLink('Cancel').click()
148 >>> browser.url148 >>> browser.url
149 'http://bugs.launchpad.dev/firefox/+bug/1'149 'http://bugs.launchpad.dev/firefox/+bug/1/'
150150
151Foo Bar wasn't subscribed to the bug.151Foo Bar wasn't subscribed to the bug.
152152
@@ -227,7 +227,7 @@
227 >>> for tag in find_tags_by_class(227 >>> for tag in find_tags_by_class(
228 ... stevea_browser.contents, 'informational message'):228 ... stevea_browser.contents, 'informational message'):
229 ... print tag.renderContents()229 ... print tag.renderContents()
230 You have been unsubscribed from this bug and 1 duplicate (<a...#2</a>)...230 You have been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
231231
232(Except for Mark, who has a structural subscription to the target,232(Except for Mark, who has a structural subscription to the target,
233there are no longer any indirect subscribers, because Steve was233there are no longer any indirect subscribers, because Steve was
@@ -258,8 +258,9 @@
258previous example.)258previous example.)
259259
260 >>> stevea_browser.open(260 >>> stevea_browser.open(
261 ... "http://launchpad.dev/tomcat/+bug/2/+subscribe")261 ... "http://launchpad.dev/tomcat/+bug/2/+addsubscriber")
262 >>> stevea_browser.getControl("Continue").click()262 >>> stevea_browser.getControl('Person').value = 'stevea'
263 >>> stevea_browser.getControl('Subscribe user').click()
263264
264 >>> stevea_browser.open(265 >>> stevea_browser.open(
265 ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")266 ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
@@ -282,7 +283,7 @@
282 >>> for tag in find_tags_by_class(283 >>> for tag in find_tags_by_class(
283 ... stevea_browser.contents, 'informational message'):284 ... stevea_browser.contents, 'informational message'):
284 ... print tag.renderContents()285 ... print tag.renderContents()
285 You have been unsubscribed from this bug and 2 duplicates (<a...#1</a>, <a...#2</a>)...286 You have been unsubscribed from bug 3 and 2 duplicates (<a...#1</a>, <a...#2</a>)...
286287
287(Let's undupe bug #1 from bug #3, since it's unneeded for the examples288(Let's undupe bug #1 from bug #3, since it's unneeded for the examples
288that follow.)289that follow.)
289290
=== modified file 'lib/lp/bugs/templates/bug-subscription.pt'
--- lib/lp/bugs/templates/bug-subscription.pt 2009-09-03 08:54:21 +0000
+++ lib/lp/bugs/templates/bug-subscription.pt 2010-09-30 22:08:08 +0000
@@ -37,7 +37,6 @@
37 <form action="." method="POST">37 <form action="." method="POST">
3838
39 <div class="field">39 <div class="field">
40
41 <div>40 <div>
42 <div tal:content="structure view/subscription_widget">41 <div tal:content="structure view/subscription_widget">
43 <input type="radio" />42 <input type="radio" />
@@ -47,6 +46,9 @@
47 </div>46 </div>
4847
49 <div class="actions">48 <div class="actions">
49 <input type="hidden" name="next_url"
50 tal:condition="view/next_url | nothing"
51 tal:attributes="value view/next_url" />
50 <input tal:condition="not: view/userIsSubscribed"52 <input tal:condition="not: view/userIsSubscribed"
51 type="submit"53 type="submit"
52 name="subscribe"54 name="subscribe"
@@ -55,7 +57,8 @@
55 type="submit"57 type="submit"
56 name="unsubscribe"58 name="unsubscribe"
57 value="Continue" />59 value="Continue" />
58 or <a tal:attributes="href context/fmt:url">Cancel</a>60 or <a tal:condition="view/cancel_url | nothing"
61 tal:attributes="href view/cancel_url">Cancel</a>
59 </div>62 </div>
60 </form>63 </form>
6164
6265
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-09-30 01:05:01 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-09-30 22:08:08 +0000
@@ -1054,6 +1054,12 @@
1054 name="+teamhierarchy"1054 name="+teamhierarchy"
1055 template="../templates/person-teamhierarchy.pt"/>1055 template="../templates/person-teamhierarchy.pt"/>
1056 <browser:page1056 <browser:page
1057 for="lp.registry.interfaces.person.IPerson"
1058 permission="zope.Public"
1059 class="lp.registry.browser.person.PersonSubscriptionsView"
1060 name="+subscriptions"
1061 template="../templates/person-subscriptions.pt"/>
1062 <browser:page
1057 for="lp.registry.interfaces.person.ITeam"1063 for="lp.registry.interfaces.person.ITeam"
1058 permission="zope.Public"1064 permission="zope.Public"
1059 class="lp.registry.browser.person.TeamIndexView"1065 class="lp.registry.browser.person.TeamIndexView"
10601066
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-27 14:19:29 +0000
+++ lib/lp/registry/browser/person.py 2010-09-30 22:08:08 +0000
@@ -53,6 +53,7 @@
53 'PersonSpecWorkloadView',53 'PersonSpecWorkloadView',
54 'PersonSpecsMenu',54 'PersonSpecsMenu',
55 'PersonSubscribedBugTaskSearchListingView',55 'PersonSubscribedBugTaskSearchListingView',
56 'PersonSubscriptionsView',
56 'PersonView',57 'PersonView',
57 'PersonVouchersView',58 'PersonVouchersView',
58 'RedirectToEditLanguagesView',59 'RedirectToEditLanguagesView',
@@ -2189,7 +2190,7 @@
2189 return "Search bugs assigned to %s" % self.context.displayname2190 return "Search bugs assigned to %s" % self.context.displayname
21902191
2191 def getSimpleSearchURL(self):2192 def getSimpleSearchURL(self):
2192 """Return a URL that can be usedas an href to the simple search."""2193 """Return a URL that can be used as an href to the simple search."""
2193 return canonical_url(self.context, view_name="+assignedbugs")2194 return canonical_url(self.context, view_name="+assignedbugs")
21942195
2195 @property2196 @property
@@ -2343,6 +2344,53 @@
2343 return self.getSearchPageHeading()2344 return self.getSearchPageHeading()
23442345
23452346
2347class PersonSubscriptionsView(BugTaskSearchListingView):
2348 """All the subscriptions for a person."""
2349
2350 page_title = 'Subscriptions'
2351
2352 def subscribedBugTasks(self):
2353 """Return a BatchNavigator for distinct bug tasks to which the
2354 person is subscribed."""
2355 bug_tasks = self.context.searchTasks(None, user=self.user,
2356 order_by='-date_last_updated',
2357 status=(BugTaskStatus.NEW,
2358 BugTaskStatus.INCOMPLETE,
2359 BugTaskStatus.CONFIRMED,
2360 BugTaskStatus.TRIAGED,
2361 BugTaskStatus.INPROGRESS,
2362 BugTaskStatus.FIXCOMMITTED,
2363 BugTaskStatus.INVALID),
2364 bug_subscriber=self.context)
2365
2366 sub_bug_tasks = []
2367 sub_bugs = []
2368
2369 for task in bug_tasks:
2370 if task.bug not in sub_bugs:
2371 sub_bug_tasks.append(task)
2372 sub_bugs.append(task.bug)
2373 return BatchNavigator(sub_bug_tasks, self.request)
2374
2375 def canUnsubscribeFromBugTasks(self):
2376 viewed_person = self.context
2377 if self.user is None:
2378 return False
2379 elif viewed_person == self.user:
2380 return True
2381 elif (viewed_person.is_team and
2382 self.user.inTeam(viewed_person)):
2383 return True
2384
2385 def getSubscriptionsPageHeading(self):
2386 """The header for the subscriptions page."""
2387 return "Subscriptions for %s" % self.context.displayname
2388
2389 @property
2390 def label(self):
2391 return self.getSubscriptionsPageHeading()
2392
2393
2346class PersonVouchersView(LaunchpadFormView):2394class PersonVouchersView(LaunchpadFormView):
2347 """Form for displaying and redeeming commercial subscription vouchers."""2395 """Form for displaying and redeeming commercial subscription vouchers."""
23482396
23492397
=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
--- lib/lp/registry/browser/tests/test_person_view.py 2010-09-16 19:31:07 +0000
+++ lib/lp/registry/browser/tests/test_person_view.py 2010-09-30 22:08:08 +0000
@@ -16,6 +16,7 @@
16from canonical.launchpad.interfaces.account import AccountStatus16from canonical.launchpad.interfaces.account import AccountStatus
17from canonical.launchpad.interfaces.logintoken import ILoginTokenSet17from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
18from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities18from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
19from canonical.launchpad.webapp.interfaces import ILaunchBag
19from canonical.launchpad.webapp.servers import LaunchpadTestRequest20from canonical.launchpad.webapp.servers import LaunchpadTestRequest
20from canonical.testing import (21from canonical.testing import (
21 DatabaseFunctionalLayer,22 DatabaseFunctionalLayer,
@@ -28,8 +29,8 @@
28from lp.registry.browser.person import (29from lp.registry.browser.person import (
29 PersonEditView,30 PersonEditView,
30 PersonView,31 PersonView,
31 TeamInvitationView,32 TeamInvitationView)
32 )33
3334
34from lp.registry.interfaces.karma import IKarmaCacheManager35from lp.registry.interfaces.karma import IKarmaCacheManager
35from lp.registry.interfaces.person import (36from lp.registry.interfaces.person import (
@@ -779,3 +780,32 @@
779 self.assertEqual(780 self.assertEqual(
780 expected,781 expected,
781 notifications[0].message)782 notifications[0].message)
783
784
785class TestSubscriptionsView(TestCaseWithFactory):
786
787 layer = LaunchpadFunctionalLayer
788
789 def setUp(self):
790 super(TestSubscriptionsView, self).setUp(
791 user='test@canonical.com')
792 self.user = getUtility(ILaunchBag).user
793 self.person = self.factory.makePerson()
794 self.other_person = self.factory.makePerson()
795 self.team = self.factory.makeTeam(owner=self.user)
796 self.team.addMember(self.person, self.user)
797
798 def test_unsubscribe_link_appears_for_user(self):
799 login_person(self.person)
800 view = create_view(self.person, '+subscriptions')
801 self.assertTrue(view.canUnsubscribeFromBugTasks())
802
803 def test_unsubscribe_link_does_not_appear_for_not_user(self):
804 login_person(self.other_person)
805 view = create_view(self.person, '+subscriptions')
806 self.assertFalse(view.canUnsubscribeFromBugTasks())
807
808 def test_unsubscribe_link_appears_for_team_member(self):
809 login_person(self.person)
810 view = create_initialized_view(self.team, '+subscriptions')
811 self.assertTrue(view.canUnsubscribeFromBugTasks())
782812
=== added file 'lib/lp/registry/stories/person/xx-person-subscriptions.txt'
--- lib/lp/registry/stories/person/xx-person-subscriptions.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/stories/person/xx-person-subscriptions.txt 2010-09-30 22:08:08 +0000
@@ -0,0 +1,131 @@
1Subscriptions View
2==================
3
4XXX: bdmurray 2010-09-24 bug=628411 This story is complete until we publish
5the link that leads to the +subscriptions page.
6
7Any user can view the direct subscriptions that a person or team has to
8blueprints, branches, bugs, merge proposals and questions.
9
10 >>> anon_browser.open('http://launchpad.dev/~ubuntu-team/+subscriptions')
11 >>> print anon_browser.title
12 Subscriptions : “Ubuntu Team” team
13
14The user can see that the Ubuntu Team does not have any direct bug
15subscriptions.
16
17 >>> page_text = extract_text(find_main_content(anon_browser.contents))
18 >>> "does not have any direct bug subscriptions" in page_text
19 True
20
21Any user can see that Sample Person is subscribed to several bugs. The bug
22subscriptions table includes the bug number, title and location.
23
24 >>> anon_browser.open('http://launchpad.dev/~name12/+subscriptions')
25 >>> print extract_text(find_tag_by_id(
26 ... anon_browser.contents, 'bug_subscriptions'))
27 Summary
28 In
29 13
30 Launchpad CSS and JS is not testible
31 Launchpad
32 4
33 Reflow problems with complex page layouts
34 Mozilla Firefox
35 9
36 Thunderbird crashes
37 thunderbird (Ubuntu)
38 1
39 Firefox does not support SVG
40 mozilla-firefox (Ubuntu)
41
42The bug subscriptions table also includes an unsubscribe link, if they have
43permission to remove the subscription, for bugs to which the person or team is
44subscribed.
45
46Sample Person can see and manage his direct bug subscriptions using the page
47too. He chooses to view bug 13 - "Launchpad CSS and JS in not testible".
48
49 >>> sample_browser = setupBrowser(auth='Basic test@canonical.com:test')
50 >>> sample_browser.open('http://bugs.launchpad.dev/~name12/+subscriptions')
51 >>> unsub_link = sample_browser.getLink(
52 ... id='unsubscribe-subscriber-12')
53 >>> unsub_link.click()
54 >>> print extract_text(find_tag_by_id(
55 ... sample_browser.contents, 'nonportlets'))
56 Unsubscribe from bug #13
57 ...
58
59After viewing the +subscribe page Sample Person decides that they still want
60to be subscribed to bug 13. They click cancel which takes them back to their
61+subscription page.
62
63 >>> cancel_link = sample_browser.getLink('Cancel')
64 >>> cancel_link.click()
65 >>> print sample_browser.title
66 Subscriptions : Sample Person
67
68He chooses to unsubscribe from bug 9 - "Thunderbird crashes".
69
70 >>> sample_browser.getLink(url='/ubuntu/+source/thunderbird/+bug/9/+subscribe').click()
71 >>> sample_browser.getControl("Unsubscribe me from this bug").selected = True
72 >>> sample_browser.getControl("Continue").click()
73 >>> print sample_browser.title
74 Subscriptions : Sample Person
75
76Sample Person can see that bug 9 is no longer listed in his direct bug
77subscriptions.
78
79 >>> sample_browser.open('http://bugs.launchpad.dev/~name12/+subscriptions')
80 >>> print extract_text(find_tag_by_id(
81 ... sample_browser.contents, 'bug_subscriptions'))
82 Summary
83 In
84 13
85 Launchpad CSS and JS is not testible
86 Launchpad
87 4
88 Reflow problems with complex page layouts
89 Mozilla Firefox
90 1
91 Firefox does not support SVG
92 mozilla-firefox (Ubuntu)
93
94Sample Person adds a bug subscription for a team, HWDB team, of which he is a
95member.
96
97 >>> sample_browser.open('http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
98 >>> sample_browser.getControl('Person').value = 'hwdb-team'
99 >>> sample_browser.getControl('Subscribe user').click()
100
101Sample Person chooses to review the subscriptions for his HWDB team.
102
103 >>> sample_browser.open('https://bugs.launchpad.dev/~hwdb-team/+subscriptions')
104 >>> print sample_browser.title
105 Subscriptions : “HWDB Team” team
106
107 >>> print extract_text(find_tag_by_id(
108 ... sample_browser.contents, 'bug_subscriptions'))
109 Summary
110 In
111 1
112 Firefox does not support SVG
113 mozilla-firefox (Ubuntu)
114
115Sample Person now chooses to unsubscribe HWDB team from bug 1.
116
117 >>> sample_browser.getLink(id='unsubscribe-subscriber-243630').click()
118 >>> print extract_text(find_tag_by_id(
119 ... sample_browser.contents, 'nonportlets'))
120 Unsubscribe from bug #1
121 ...
122
123 >>> sample_browser.getControl(
124 ... 'Unsubscribe HWDB Team from this bug').selected = True
125 >>> sample_browser.getControl('Continue').click()
126
127 >>> sample_browser.open('https://launchpad.dev/~hwdb-team/+subscriptions')
128 >>> page_text = extract_text(
129 ... find_main_content(sample_browser.contents))
130 >>> "does not have any direct bug subscriptions" in page_text
131 True
0132
=== modified file 'lib/lp/registry/stories/productseries/xx-productseries-delete.txt'
--- lib/lp/registry/stories/productseries/xx-productseries-delete.txt 2010-03-03 00:40:03 +0000
+++ lib/lp/registry/stories/productseries/xx-productseries-delete.txt 2010-09-30 22:08:08 +0000
@@ -32,7 +32,7 @@
32 LookupError: ...32 LookupError: ...
3333
34Sample person chooses to cancel the action and make the 1.0 series the focus34Sample person chooses to cancel the action and make the 1.0 series the focus
35of development. He then removes the linked package which he beleives is35of development. He then removes the linked package which he believes is
36bogus.36bogus.
3737
38 >>> owner_browser.getLink('Cancel').click()38 >>> owner_browser.getLink('Cancel').click()
3939
=== added file 'lib/lp/registry/templates/person-subscriptions.pt'
--- lib/lp/registry/templates/person-subscriptions.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/person-subscriptions.pt 2010-09-30 22:08:08 +0000
@@ -0,0 +1,73 @@
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 xml:lang="en"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="view/macro:page/main_side"
10 i18n:domain="launchpad"
11>
12 <body>
13
14 <div metal:fill-slot="main"
15 tal:define="sub_bugs view/subscribedBugTasks;
16 sub_bug_batch sub_bugs/currentBatch">
17 <tal:no_subscribed_bugs condition="not: sub_bug_batch|nothing">
18 <p><tal:person content="context/fmt:displayname">Sample
19 Person</tal:person> does not have any direct bug subscriptions.</p>
20 </tal:no_subscribed_bugs>
21
22 <tal:subscribed_bugs condition="sub_bug_batch|nothing">
23 <p>
24 Direct bug subscriptions for
25 <tal:person content="context/fmt:displayname">Sample Person</tal:person>
26 </p>
27 <div tal:replace="structure sub_bugs/@@+navigation-links-lower"/>
28 <table id="bug_subscriptions" class="listing">
29 <thead>
30 <tr>
31 <th colspan="3">Summary</th>
32 <th>
33 In
34 </th>
35 <th tal:condition="view/canUnsubscribeFromBugTasks">
36 </th>
37 </tr>
38 </thead>
39 <tr tal:repeat="sub_bug_task sub_bug_batch">
40 <td class="icon left">
41 <span tal:replace="structure sub_bug_task/image:icon" />
42 </td>
43 <td id="bug_number" style="text-align: right">
44 <span tal:replace="sub_bug_task/bug/id" />
45 </td>
46 <td>
47 <a tal:attributes="href sub_bug_task/fmt:url"
48 tal:content="sub_bug_task/bug/title" />
49 </td>
50 <td tal:content="sub_bug_task/bugtargetdisplayname"
51 tal:condition="sub_bug_task/bugtargetdisplayname|nothing"
52 >mozilla-firefox (Ubuntu)</td>
53 <td tal:condition="view/canUnsubscribeFromBugTasks"
54 align="center">
55 <a
56 tal:attributes="
57 title string:Unsubscribe ${context/fmt:displayname} from bug ${sub_bug_task/bug/id};
58 id string:unsubscribe-subscriber-${context/id};
59 href string:${sub_bug_task/fmt:url}/+subscribe;
60 "
61 >
62 <img src="/@@/edit"
63 tal:attributes="id string:unsubscribe-icon-${context/id};
64 position string:relative"/>
65 </a>
66 </td>
67 </tr>
68 </table>
69 </tal:subscribed_bugs>
70
71 </div>
72</body>
73</html>