Merge lp:~bac/launchpad/bug-799901 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 13487
Proposed branch: lp:~bac/launchpad/bug-799901
Merge into: lp:launchpad
Diff against target: 504 lines (+170/-48)
8 files modified
lib/lp/bugs/browser/bugsubscription.py (+21/-12)
lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+79/-12)
lib/lp/bugs/doc/bugsubscription.txt (+6/-6)
lib/lp/bugs/javascript/subscribers_list.js (+9/-5)
lib/lp/bugs/javascript/tests/test_subscribers_list.js (+24/-3)
lib/lp/bugs/model/bug.py (+3/-1)
lib/lp/bugs/model/bugsubscription.py (+6/-4)
lib/lp/bugs/model/tests/test_bug.py (+22/-5)
To merge this branch: bzr merge lp:~bac/launchpad/bug-799901
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+68590@code.launchpad.net

Commit message

[r=allenap][bug=799901] Add tooltip to bug subscribers list.

Description of the change

= Summary =

The new bug subscription work has a regression in that the tooltip showing who subscribed a person or team is missing. This branch adds that functionality back.

== Proposed fix ==

Add the tooltip for direct subscribers. The indirect subscribers (seen under "May be notified") don't have the required subscription information and were not previously decorated.

Note that I have chose to not include a tooltip when using "Subscribe someone else". Since the user just did the action it is forgiveable to not display the fact that he just did it. Adding that information would require another roundtrip to fetch the extra data.

== Pre-implementation notes ==

Chat with Gary.

== Implementation details ==

As above.

== Tests ==

bin/test -vvt test_bugsubscription_views
firefox lib/lp/bugs/javascript/tests/test_subscribers_list.html

== Demo and Q/A ==

Go to https://launchpad.dev/bugs/1 and view the subscriber list.

= Launchpad lint =

Will fix this lint before landing.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/model/bugsubscription.py
  lib/lp/bugs/doc/bugsubscription.txt
  lib/lp/bugs/browser/tests/test_bugsubscription_views.py
  lib/lp/bugs/browser/bugsubscription.py
  lib/lp/bugs/javascript/subscribers_list.js
  lib/lp/bugs/javascript/tests/test_subscribers_list.js

./lib/lp/bugs/javascript/subscribers_list.js
     725: Line exceeds 78 characters.
./lib/lp/bugs/javascript/tests/test_subscribers_list.js
     791: Line exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

I haven't read the diff here in whole, but I'd like to note that this appears as though it will trigger late evaluation - the display_subscribed_by property isn't eager loaded, and a bug with a couple hundred subscriptions will cause eager loading of a couple hundred person objects *unless* they are all self-subscribed. It will also be a little slow because its comparing objects not keys - the property does:
        if self.person == self.subscribed_by:
            return u'Self-subscribed'
        else:
            return u'Subscribed by %s' % self.subscribed_by.displayname

This would be a little more efficient as

       if self.person_id == self.subscribed_by_id:
            return u'Self-subscribed'
        else:
            return u'Subscribed by %s' % self.subscribed_by.displayname

but the big thing is eager loading: we need to populate the subscribing person (but only for views that need it) en masse, rather than just-in-time.

I don't know if this will cause timeouts, but this sort of thing is one of the key drivers for timeouts.

Revision history for this message
Данило Шеган (danilo) wrote :

Indeed, Robert is right. For that sole reason I implemented a separate method which gets all the data with one query for this page, and that method needs extending: IBug.getDirectSubscribersWithDetails()

It returns a ResultSet of (Person, Subscription) tuples, and should be extended to return (Person, SubscribedBy, Subscription) tuples where SubscribedBy is a ClassAlias(Person) as appropriate.

Don't worry about changing the method on the model because this is the only place it is used in.

Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the suggestion Danilos. I've made the change you suggested. I did go ahead and update the model as Robert suggested just because I think it is better practice to do the comparisons the most efficient way even if the objects are cached so that someone else wouldn't pattern a new change off the inefficient approach.

I also added a test showing the query count for getting the direct subscribers is 1. This required a slight refactoring of the browser property.

Revision history for this message
Gavin Panella (allenap) wrote :

Tip top :)

[1]

+ if IBug.providedBy(self.context):
+ bug = self.context
+ elif IBugTask.providedBy(self.context):
+ bug = self.context.bug

I know you only moved this, but I thought I'd mention that there is an
adapter from IBugTask to IBug, so the above can be written as:

    bug = IBug(self.context)

[2]

+ harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)

Cool, I've not see that before.

[3]

+ # A subscriber_data_js returns JSON string of a list

That should probably read:

        # subscriber_data_js returns a JSON string of a list

or something like that.

[4]

In the subscriber dict, assembled in direct_subscriber_data,
subscribed_by /feels/ like it should be the person, rather than a
description. Consider doing instead something like:

            subscriber = {
                'name': person.name,
                [...]
                'display_subscribed_by': subscription.display_subscribed_by,
                'subscribed_by_link': absoluteURL(
                    subscription.subscribed_by, self.api_request),
                }

That makes the data more generally useful, and it more closely
resembles the BugSubscription API.

Ach, it's not important, just an idea.

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

Another minor note from me (I've had the tab loaded, sorry :)): perhaps you can also move the display_subscribed_by into the view and not rely on storm caching since you've already have both subscriber and subscribed_by objects which you can directly compare?

Otherwise, looks great!

Revision history for this message
Данило Шеган (danilo) wrote :

Of course, it's too late for it now (I see it's merged already, and I haven't refreshed the tab recently). So, never mind it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
--- lib/lp/bugs/browser/bugsubscription.py 2011-06-29 13:41:36 +0000
+++ lib/lp/bugs/browser/bugsubscription.py 2011-07-21 18:09:56 +0000
@@ -49,7 +49,6 @@
49from lp.bugs.enum import BugNotificationLevel49from lp.bugs.enum import BugNotificationLevel
50from lp.bugs.interfaces.bug import IBug50from lp.bugs.interfaces.bug import IBug
51from lp.bugs.interfaces.bugsubscription import IBugSubscription51from lp.bugs.interfaces.bugsubscription import IBugSubscription
52from lp.bugs.interfaces.bugtask import IBugTask
53from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions52from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions
54from lp.bugs.model.structuralsubscription import (53from lp.bugs.model.structuralsubscription import (
55 get_structural_subscriptions_for_bug,54 get_structural_subscriptions_for_bug,
@@ -529,17 +528,19 @@
529class BugPortletSubscribersWithDetails(LaunchpadView):528class BugPortletSubscribersWithDetails(LaunchpadView):
530 """A view that returns a JSON dump of the subscriber details for a bug."""529 """A view that returns a JSON dump of the subscriber details for a bug."""
531530
532 @property531 @cachedproperty
533 def subscriber_data_js(self):532 def api_request(self):
534 """Return subscriber_ids in a form suitable for JavaScript use."""533 return IWebServiceClientRequest(self.request)
534
535 def direct_subscriber_data(self, bug):
536 """Get the direct subscriber data.
537
538 This method is isolated from the subscriber_data_js so that query
539 count testing can be done accurately and robustly.
540 """
535 data = []541 data = []
536 if IBug.providedBy(self.context):
537 bug = self.context
538 elif IBugTask.providedBy(self.context):
539 bug = self.context.bug
540 details = list(bug.getDirectSubscribersWithDetails())542 details = list(bug.getDirectSubscribersWithDetails())
541 api_request = IWebServiceClientRequest(self.request)543 for person, subscribed_by, subscription in details:
542 for person, subscription in details:
543 can_edit = subscription.canBeUnsubscribedByUser(self.user)544 can_edit = subscription.canBeUnsubscribedByUser(self.user)
544 if person == self.user or (person.private and not can_edit):545 if person == self.user or (person.private and not can_edit):
545 # Skip the current user viewing the page,546 # Skip the current user viewing the page,
@@ -550,9 +551,10 @@
550 'name': person.name,551 'name': person.name,
551 'display_name': person.displayname,552 'display_name': person.displayname,
552 'web_link': canonical_url(person, rootsite='mainsite'),553 'web_link': canonical_url(person, rootsite='mainsite'),
553 'self_link': absoluteURL(person, api_request),554 'self_link': absoluteURL(person, self.api_request),
554 'is_team': person.is_team,555 'is_team': person.is_team,
555 'can_edit': can_edit,556 'can_edit': can_edit,
557 'display_subscribed_by': subscription.display_subscribed_by,
556 }558 }
557 record = {559 record = {
558 'subscriber': subscriber,560 'subscriber': subscriber,
@@ -560,6 +562,13 @@
560 removeSecurityProxy(subscription.bug_notification_level)),562 removeSecurityProxy(subscription.bug_notification_level)),
561 }563 }
562 data.append(record)564 data.append(record)
565 return data
566
567 @property
568 def subscriber_data_js(self):
569 """Return subscriber_ids in a form suitable for JavaScript use."""
570 bug = IBug(self.context)
571 data = self.direct_subscriber_data(bug)
563572
564 others = list(bug.getIndirectSubscribers())573 others = list(bug.getIndirectSubscribers())
565 for person in others:574 for person in others:
@@ -570,7 +579,7 @@
570 'name': person.name,579 'name': person.name,
571 'display_name': person.displayname,580 'display_name': person.displayname,
572 'web_link': canonical_url(person, rootsite='mainsite'),581 'web_link': canonical_url(person, rootsite='mainsite'),
573 'self_link': absoluteURL(person, api_request),582 'self_link': absoluteURL(person, self.api_request),
574 'is_team': person.is_team,583 'is_team': person.is_team,
575 'can_edit': False,584 'can_edit': False,
576 }585 }
577586
=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-30 22:41:02 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-07-21 18:09:56 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for BugSubscription views."""4"""Tests for BugSubscription views."""
@@ -6,7 +6,8 @@
6__metaclass__ = type6__metaclass__ = type
77
8from simplejson import dumps8from simplejson import dumps
99from storm.store import Store
10from testtools.matchers import Equals
10from zope.component import getUtility11from zope.component import getUtility
11from zope.traversing.browser import absoluteURL12from zope.traversing.browser import absoluteURL
1213
@@ -23,8 +24,10 @@
23from lp.registry.interfaces.person import IPersonSet24from lp.registry.interfaces.person import IPersonSet
24from lp.testing import (25from lp.testing import (
25 person_logged_in,26 person_logged_in,
27 StormStatementRecorder,
26 TestCaseWithFactory,28 TestCaseWithFactory,
27 )29 )
30from lp.testing.matchers import HasQueryCount
28from lp.testing.sampledata import ADMIN_EMAIL31from lp.testing.sampledata import ADMIN_EMAIL
29from lp.testing.views import create_initialized_view32from lp.testing.views import create_initialized_view
3033
@@ -467,7 +470,7 @@
467 self.assertEqual(dumps([]), harness.view.subscriber_data_js)470 self.assertEqual(dumps([]), harness.view.subscriber_data_js)
468471
469 def test_data_person_subscription(self):472 def test_data_person_subscription(self):
470 # A subscriber_data_js returns JSON string of a list473 # subscriber_data_js returns JSON string of a list
471 # containing all subscriber information needed for474 # containing all subscriber information needed for
472 # subscribers_list.js subscribers loading.475 # subscribers_list.js subscribers loading.
473 bug = self._makeBugWithNoSubscribers()476 bug = self._makeBugWithNoSubscribers()
@@ -487,18 +490,69 @@
487 'can_edit': False,490 'can_edit': False,
488 'web_link': canonical_url(subscriber),491 'web_link': canonical_url(subscriber),
489 'self_link': absoluteURL(subscriber, api_request),492 'self_link': absoluteURL(subscriber, api_request),
490 },493 'display_subscribed_by': 'Self-subscribed',
491 'subscription_level': "Lifecycle",494 },
492 }495 'subscription_level': "Lifecycle",
493 self.assertEqual(496 }
494 dumps([expected_result]), harness.view.subscriber_data_js)497 self.assertEqual(
498 dumps([expected_result]), harness.view.subscriber_data_js)
499
500 def test_data_person_subscription_other_subscriber(self):
501 # Ensure subscriber_data_js does the correct thing when the person who
502 # did the subscribing is someone else.
503 bug = self._makeBugWithNoSubscribers()
504 subscribed_by = self.factory.makePerson(
505 name="someone", displayname='Someone')
506 subscriber = self.factory.makePerson(
507 name='user', displayname='Subscriber Name')
508 with person_logged_in(subscriber):
509 bug.subscribe(person=subscriber,
510 subscribed_by=subscribed_by,
511 level=BugNotificationLevel.LIFECYCLE)
512 harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
513 api_request = IWebServiceClientRequest(harness.request)
514
515 expected_result = {
516 'subscriber': {
517 'name': 'user',
518 'display_name': 'Subscriber Name',
519 'is_team': False,
520 'can_edit': False,
521 'web_link': canonical_url(subscriber),
522 'self_link': absoluteURL(subscriber, api_request),
523 'display_subscribed_by': 'Subscribed by Someone (someone)',
524 },
525 'subscription_level': "Lifecycle",
526 }
527 self.assertEqual(
528 dumps([expected_result]), harness.view.subscriber_data_js)
529
530 def test_data_person_subscription_other_subscriber_query_count(self):
531 # All subscriber data should be retrieved with a single query.
532 bug = self._makeBugWithNoSubscribers()
533 subscribed_by = self.factory.makePerson(
534 name="someone", displayname='Someone')
535 subscriber = self.factory.makePerson(
536 name='user', displayname='Subscriber Name')
537 with person_logged_in(subscriber):
538 bug.subscribe(person=subscriber,
539 subscribed_by=subscribed_by,
540 level=BugNotificationLevel.LIFECYCLE)
541 harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
542 # Invoke the view method, ignoring the results.
543 Store.of(bug).invalidate()
544 with StormStatementRecorder() as recorder:
545 harness.view.direct_subscriber_data(bug)
546 self.assertThat(recorder, HasQueryCount(Equals(1)))
495547
496 def test_data_team_subscription(self):548 def test_data_team_subscription(self):
497 # For a team subscription, subscriber_data_js has is_team set549 # For a team subscription, subscriber_data_js has is_team set
498 # to true.550 # to true.
499 bug = self._makeBugWithNoSubscribers()551 bug = self._makeBugWithNoSubscribers()
552 teamowner = self.factory.makePerson(
553 name="team-owner", displayname="Team Owner")
500 subscriber = self.factory.makeTeam(554 subscriber = self.factory.makeTeam(
501 name='team', displayname='Team Name')555 name='team', displayname='Team Name', owner=teamowner)
502 with person_logged_in(subscriber.teamowner):556 with person_logged_in(subscriber.teamowner):
503 bug.subscribe(subscriber, subscriber.teamowner,557 bug.subscribe(subscriber, subscriber.teamowner,
504 level=BugNotificationLevel.LIFECYCLE)558 level=BugNotificationLevel.LIFECYCLE)
@@ -513,6 +567,8 @@
513 'can_edit': False,567 'can_edit': False,
514 'web_link': canonical_url(subscriber),568 'web_link': canonical_url(subscriber),
515 'self_link': absoluteURL(subscriber, api_request),569 'self_link': absoluteURL(subscriber, api_request),
570 'display_subscribed_by': \
571 'Subscribed by Team Owner (team-owner)',
516 },572 },
517 'subscription_level': "Lifecycle",573 'subscription_level': "Lifecycle",
518 }574 }
@@ -523,8 +579,10 @@
523 # For a team subscription, subscriber_data_js has can_edit579 # For a team subscription, subscriber_data_js has can_edit
524 # set to true for team owner.580 # set to true for team owner.
525 bug = self._makeBugWithNoSubscribers()581 bug = self._makeBugWithNoSubscribers()
582 teamowner = self.factory.makePerson(
583 name="team-owner", displayname="Team Owner")
526 subscriber = self.factory.makeTeam(584 subscriber = self.factory.makeTeam(
527 name='team', displayname='Team Name')585 name='team', displayname='Team Name', owner=teamowner)
528 with person_logged_in(subscriber.teamowner):586 with person_logged_in(subscriber.teamowner):
529 bug.subscribe(subscriber, subscriber.teamowner,587 bug.subscribe(subscriber, subscriber.teamowner,
530 level=BugNotificationLevel.LIFECYCLE)588 level=BugNotificationLevel.LIFECYCLE)
@@ -540,6 +598,8 @@
540 'can_edit': True,598 'can_edit': True,
541 'web_link': canonical_url(subscriber),599 'web_link': canonical_url(subscriber),
542 'self_link': absoluteURL(subscriber, api_request),600 'self_link': absoluteURL(subscriber, api_request),
601 'display_subscribed_by': \
602 'Subscribed by Team Owner (team-owner)',
543 },603 },
544 'subscription_level': "Lifecycle",604 'subscription_level': "Lifecycle",
545 }605 }
@@ -552,8 +612,11 @@
552 # set to true for team member.612 # set to true for team member.
553 bug = self._makeBugWithNoSubscribers()613 bug = self._makeBugWithNoSubscribers()
554 member = self.factory.makePerson()614 member = self.factory.makePerson()
615 teamowner = self.factory.makePerson(
616 name="team-owner", displayname="Team Owner")
555 subscriber = self.factory.makeTeam(617 subscriber = self.factory.makeTeam(
556 name='team', displayname='Team Name', members=[member])618 name='team', displayname='Team Name', owner=teamowner,
619 members=[member])
557 with person_logged_in(subscriber.teamowner):620 with person_logged_in(subscriber.teamowner):
558 bug.subscribe(subscriber, subscriber.teamowner,621 bug.subscribe(subscriber, subscriber.teamowner,
559 level=BugNotificationLevel.LIFECYCLE)622 level=BugNotificationLevel.LIFECYCLE)
@@ -569,6 +632,8 @@
569 'can_edit': True,632 'can_edit': True,
570 'web_link': canonical_url(subscriber),633 'web_link': canonical_url(subscriber),
571 'self_link': absoluteURL(subscriber, api_request),634 'self_link': absoluteURL(subscriber, api_request),
635 'display_subscribed_by': \
636 'Subscribed by Team Owner (team-owner)',
572 },637 },
573 'subscription_level': "Lifecycle",638 'subscription_level': "Lifecycle",
574 }639 }
@@ -598,6 +663,7 @@
598 'can_edit': True,663 'can_edit': True,
599 'web_link': canonical_url(subscriber),664 'web_link': canonical_url(subscriber),
600 'self_link': absoluteURL(subscriber, api_request),665 'self_link': absoluteURL(subscriber, api_request),
666 'display_subscribed_by': 'Self-subscribed',
601 },667 },
602 'subscription_level': "Lifecycle",668 'subscription_level': "Lifecycle",
603 }669 }
@@ -615,7 +681,7 @@
615 subscriber = self.factory.makePerson(681 subscriber = self.factory.makePerson(
616 name='user', displayname='Subscriber Name')682 name='user', displayname='Subscriber Name')
617 subscribed_by = self.factory.makePerson(683 subscribed_by = self.factory.makePerson(
618 name='someone', displayname='Subscribed By Name')684 name='someone', displayname='Someone')
619 with person_logged_in(subscriber):685 with person_logged_in(subscriber):
620 bug.subscribe(subscriber, subscribed_by,686 bug.subscribe(subscriber, subscribed_by,
621 level=BugNotificationLevel.LIFECYCLE)687 level=BugNotificationLevel.LIFECYCLE)
@@ -630,6 +696,7 @@
630 'can_edit': True,696 'can_edit': True,
631 'web_link': canonical_url(subscriber),697 'web_link': canonical_url(subscriber),
632 'self_link': absoluteURL(subscriber, api_request),698 'self_link': absoluteURL(subscriber, api_request),
699 'display_subscribed_by': 'Subscribed by Someone (someone)',
633 },700 },
634 'subscription_level': "Lifecycle",701 'subscription_level': "Lifecycle",
635 }702 }
636703
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2011-06-23 21:46:48 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2011-07-21 18:09:56 +0000
@@ -888,14 +888,14 @@
888 >>> ff_bug.subscribe(lifeless, mark)888 >>> ff_bug.subscribe(lifeless, mark)
889 <lp.bugs.model.bugsubscription.BugSubscription ...>889 <lp.bugs.model.bugsubscription.BugSubscription ...>
890 >>> subscriptions = [890 >>> subscriptions = [
891 ... "%s (%s)" % (891 ... "%s: %s" % (
892 ... subscription.person.displayname,892 ... subscription.person.displayname,
893 ... subscription.display_subscribed_by)893 ... subscription.display_subscribed_by)
894 ... for subscription in ff_bug.getDirectSubscriptions()]894 ... for subscription in ff_bug.getDirectSubscriptions()]
895 >>> for subscription in sorted(subscriptions):895 >>> for subscription in sorted(subscriptions):
896 ... print subscription896 ... print subscription
897 Mark Shuttleworth (Self-subscribed)897 Mark Shuttleworth: Self-subscribed
898 Robert Collins (Subscribed by Mark Shuttleworth)898 Robert Collins: Subscribed by Mark Shuttleworth (mark)
899 >>> params = CreateBugParams(899 >>> params = CreateBugParams(
900 ... title="one more dupe test bug",900 ... title="one more dupe test bug",
901 ... comment="one more dupe test description",901 ... comment="one more dupe test description",
@@ -906,8 +906,8 @@
906 >>> dupe_ff_bug.subscribe(foobar, lifeless)906 >>> dupe_ff_bug.subscribe(foobar, lifeless)
907 <lp.bugs.model.bugsubscription.BugSubscription ...>907 <lp.bugs.model.bugsubscription.BugSubscription ...>
908 >>> for subscription in ff_bug.getSubscriptionsFromDuplicates():908 >>> for subscription in ff_bug.getSubscriptionsFromDuplicates():
909 ... print '%s (%s)' % (909 ... print '%s: %s' % (
910 ... subscription.person.displayname,910 ... subscription.person.displayname,
911 ... subscription.display_duplicate_subscribed_by)911 ... subscription.display_duplicate_subscribed_by)
912 Scott James Remnant (Self-subscribed to bug ...)912 Scott James Remnant: Self-subscribed to bug ...
913 Foo Bar (Subscribed to bug ... by Robert Collins)913 Foo Bar: Subscribed to bug ... by Robert Collins (lifeless)
914914
=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
--- lib/lp/bugs/javascript/subscribers_list.js 2011-07-20 13:35:24 +0000
+++ lib/lp/bugs/javascript/subscribers_list.js 2011-07-21 18:09:56 +0000
@@ -192,8 +192,9 @@
192 * "name": "foobar",192 * "name": "foobar",
193 * "display_name": "Foo Bar",193 * "display_name": "Foo Bar",
194 * "can_edit": true/false,194 * "can_edit": true/false,
195 * "is_team": false/false,195 * "is_team": true/false,
196 * "web_link": "https://launchpad.dev/~foobar"196 * "web_link": "https://launchpad.dev/~foobar",
197 * "display_subscribed_by": "Matt Zimmerman (mdz)"
197 * },198 * },
198 * "subscription_level": "Details"},199 * "subscription_level": "Details"},
199 * { "subscriber": ... }200 * { "subscriber": ... }
@@ -322,7 +323,6 @@
322 var subscriber = person.getAttrs();323 var subscriber = person.getAttrs();
323 this.subscribers_list.addSubscriber(subscriber, 'Discussion');324 this.subscribers_list.addSubscriber(subscriber, 'Discussion');
324 this.subscribers_list.indicateSubscriberActivity(subscriber);325 this.subscribers_list.indicateSubscriberActivity(subscriber);
325
326 var loader = this;326 var loader = this;
327327
328 function on_success() {328 function on_success() {
@@ -687,7 +687,7 @@
687 *687 *
688 * @method _createSubscriberNode688 * @method _createSubscriberNode
689 * @param subscriber {Object} Object containing `name`, `display_name`689 * @param subscriber {Object} Object containing `name`, `display_name`
690 * `web_link` and `is_team` attributes.690 * `web_link`, `is_team` and `display_subscribed_by` attributes.
691 * @return {Object} Node containing a subscriber link.691 * @return {Object} Node containing a subscriber link.
692 */692 */
693SubscribersList.prototype._createSubscriberNode = function(subscriber) {693SubscribersList.prototype._createSubscriberNode = function(subscriber) {
@@ -712,6 +712,9 @@
712 } else {712 } else {
713 subscriber_text.addClass('person');713 subscriber_text.addClass('person');
714 }714 }
715 if (Y.Lang.isString(subscriber.display_subscribed_by)) {
716 subscriber_link.set('title', subscriber.display_subscribed_by);
717 }
715 subscriber_link.appendChild(subscriber_text);718 subscriber_link.appendChild(subscriber_text);
716719
717 subscriber_node.appendChild(subscriber_link);720 subscriber_node.appendChild(subscriber_link);
@@ -728,7 +731,8 @@
728 *731 *
729 * @method addSubscriber732 * @method addSubscriber
730 * @param subscriber {Object} Object containing `name`, `display_name`733 * @param subscriber {Object} Object containing `name`, `display_name`
731 * `web_link` and `is_team` attributes describing the subscriber.734 * `web_link`, `is_team`, and `display_subscribed_by` attributes describing
735 * the subscriber.
732 * @param level {String} Level of the subscription.736 * @param level {String} Level of the subscription.
733 * @param config {Object} Object containing potential 'unsubscribe' callback737 * @param config {Object} Object containing potential 'unsubscribe' callback
734 * in the `unsubscribe_callback` attribute.738 * in the `unsubscribe_callback` attribute.
735739
=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-07-20 13:35:24 +0000
+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-07-21 18:09:56 +0000
@@ -780,18 +780,39 @@
780 var subscriber = {780 var subscriber = {
781 name: 'user',781 name: 'user',
782 display_name: 'User Name',782 display_name: 'User Name',
783 web_link: 'http://launchpad.net/~user'783 web_link: 'http://launchpad.net/~user',
784 display_subscribed_by: 'Subscribed by Someone (someone)'
784 };785 };
785 var node = subscribers_list._createSubscriberNode(subscriber);786 var node = subscribers_list._createSubscriberNode(subscriber);
786 Y.Assert.isTrue(node.hasClass('subscriber'));787 Y.Assert.isTrue(node.hasClass('subscriber'));
787788
788 var link = node.one('a');789 var link = node.one('a');
789 Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));790 Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));
790791 Y.Assert.areEqual(
792 'Subscribed by Someone (someone)', link.get('title'));
791 var text = link.one('span');793 var text = link.one('span');
792 Y.Assert.areEqual('User Name', text.get('text'));794 Y.Assert.areEqual('User Name', text.get('text'));
793 Y.Assert.isTrue(text.hasClass('sprite'));795 Y.Assert.isTrue(text.hasClass('sprite'));
794 Y.Assert.isTrue(text.hasClass('person'));796 Y.Assert.isTrue(text.hasClass('person'));
797
798 },
799
800 test_createSubscriberNode_missing_display_subscribed_by: function() {
801 // When passed a subscriber object with no 'display_subscribed_by'
802 // attribute then the title is simply not set but shows up
803 // as a null string.
804 var subscribers_list = setUpSubscribersList(this.root);
805 var subscriber = {
806 name: 'user',
807 display_name: 'User Name',
808 web_link: 'http://launchpad.net/~user'
809 };
810 var node = subscribers_list._createSubscriberNode(subscriber);
811 Y.Assert.isTrue(node.hasClass('subscriber'));
812
813 var link = node.one('a');
814 Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));
815 Y.Assert.areEqual('', link.get('title'));
795 },816 },
796817
797 test_createSubscriberNode_display_name_truncated: function() {818 test_createSubscriberNode_display_name_truncated: function() {
@@ -827,7 +848,7 @@
827848
828 test_addSubscriber: function() {849 test_addSubscriber: function() {
829 // When there is no subscriber in the subscriber list,850 // When there is no subscriber in the subscriber list,
830 // new node is constructed and appropriate section is added.851 // a new node is constructed and the appropriate section is added.
831 var subscribers_list = setUpSubscribersList(this.root);852 var subscribers_list = setUpSubscribersList(this.root);
832 var node = subscribers_list.addSubscriber(853 var node = subscribers_list.addSubscriber(
833 { name: 'user' }, 'Details');854 { name: 'user' }, 'Details');
834855
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2011-07-19 20:34:27 +0000
+++ lib/lp/bugs/model/bug.py 2011-07-21 18:09:56 +0000
@@ -939,10 +939,12 @@
939939
940 def getDirectSubscribersWithDetails(self):940 def getDirectSubscribersWithDetails(self):
941 """See `IBug`."""941 """See `IBug`."""
942 SubscribedBy = ClassAlias(Person, name="subscribed_by")
942 results = Store.of(self).find(943 results = Store.of(self).find(
943 (Person, BugSubscription),944 (Person, SubscribedBy, BugSubscription),
944 BugSubscription.person_id == Person.id,945 BugSubscription.person_id == Person.id,
945 BugSubscription.bug_id == self.id,946 BugSubscription.bug_id == self.id,
947 BugSubscription.subscribed_by_id == SubscribedBy.id,
946 Not(In(BugSubscription.person_id,948 Not(In(BugSubscription.person_id,
947 Select(BugMute.person_id, BugMute.bug_id == self.id)))949 Select(BugMute.person_id, BugMute.bug_id == self.id)))
948 ).order_by(Person.displayname)950 ).order_by(Person.displayname)
949951
=== modified file 'lib/lp/bugs/model/bugsubscription.py'
--- lib/lp/bugs/model/bugsubscription.py 2011-06-23 04:55:46 +0000
+++ lib/lp/bugs/model/bugsubscription.py 2011-07-21 18:09:56 +0000
@@ -62,10 +62,11 @@
62 @property62 @property
63 def display_subscribed_by(self):63 def display_subscribed_by(self):
64 """See `IBugSubscription`."""64 """See `IBugSubscription`."""
65 if self.person == self.subscribed_by:65 if self.person_id == self.subscribed_by_id:
66 return u'Self-subscribed'66 return u'Self-subscribed'
67 else:67 else:
68 return u'Subscribed by %s' % self.subscribed_by.displayname68 return u'Subscribed by %s (%s)' % (
69 self.subscribed_by.displayname, self.subscribed_by.name)
6970
70 @property71 @property
71 def display_duplicate_subscribed_by(self):72 def display_duplicate_subscribed_by(self):
@@ -73,8 +74,9 @@
73 if self.person == self.subscribed_by:74 if self.person == self.subscribed_by:
74 return u'Self-subscribed to bug %s' % (self.bug_id)75 return u'Self-subscribed to bug %s' % (self.bug_id)
75 else:76 else:
76 return u'Subscribed to bug %s by %s' % (self.bug_id,77 return u'Subscribed to bug %s by %s (%s)' % (
77 self.subscribed_by.displayname)78 self.bug_id, self.subscribed_by.displayname,
79 self.subscribed_by.name)
7880
79 def canBeUnsubscribedByUser(self, user):81 def canBeUnsubscribedByUser(self, user):
80 """See `IBugSubscription`."""82 """See `IBugSubscription`."""
8183
=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
--- lib/lp/bugs/model/tests/test_bug.py 2011-07-19 14:42:52 +0000
+++ lib/lp/bugs/model/tests/test_bug.py 2011-07-21 18:09:56 +0000
@@ -205,9 +205,27 @@
205 set(subscribers), set(direct_subscribers),205 set(subscribers), set(direct_subscribers),
206 "Subscribers did not match expected value.")206 "Subscribers did not match expected value.")
207207
208 def test_get_direct_subscribers_with_details(self):208 def test_get_direct_subscribers_with_details_other_subscriber(self):
209 # getDirectSubscribersWithDetails() returns both209 # getDirectSubscribersWithDetails() returns
210 # Person and BugSubscription records in one go.210 # Person and BugSubscription records in one go as well as the
211 # BugSubscription.subscribed_by person.
212 bug = self.factory.makeBug()
213 with person_logged_in(bug.owner):
214 # Unsubscribe bug owner so it doesn't taint the result.
215 bug.unsubscribe(bug.owner, bug.owner)
216 subscriber = self.factory.makePerson()
217 subscribee = self.factory.makePerson()
218 with person_logged_in(subscriber):
219 subscription = bug.subscribe(
220 subscribee, subscriber, level=BugNotificationLevel.LIFECYCLE)
221 self.assertContentEqual(
222 [(subscribee, subscriber, subscription)],
223 bug.getDirectSubscribersWithDetails())
224
225 def test_get_direct_subscribers_with_details_self_subscribed(self):
226 # getDirectSubscribersWithDetails() returns
227 # Person and BugSubscription records in one go as well as the
228 # BugSubscription.subscribed_by person.
211 bug = self.factory.makeBug()229 bug = self.factory.makeBug()
212 with person_logged_in(bug.owner):230 with person_logged_in(bug.owner):
213 # Unsubscribe bug owner so it doesn't taint the result.231 # Unsubscribe bug owner so it doesn't taint the result.
@@ -216,9 +234,8 @@
216 with person_logged_in(subscriber):234 with person_logged_in(subscriber):
217 subscription = bug.subscribe(235 subscription = bug.subscribe(
218 subscriber, subscriber, level=BugNotificationLevel.LIFECYCLE)236 subscriber, subscriber, level=BugNotificationLevel.LIFECYCLE)
219
220 self.assertContentEqual(237 self.assertContentEqual(
221 [(subscriber, subscription)],238 [(subscriber, subscriber, subscription)],
222 bug.getDirectSubscribersWithDetails())239 bug.getDirectSubscribersWithDetails())
223240
224 def test_get_direct_subscribers_with_details_mute_excludes(self):241 def test_get_direct_subscribers_with_details_mute_excludes(self):