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
1=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
2--- lib/lp/bugs/browser/bugsubscription.py 2011-06-29 13:41:36 +0000
3+++ lib/lp/bugs/browser/bugsubscription.py 2011-07-21 18:09:56 +0000
4@@ -49,7 +49,6 @@
5 from lp.bugs.enum import BugNotificationLevel
6 from lp.bugs.interfaces.bug import IBug
7 from lp.bugs.interfaces.bugsubscription import IBugSubscription
8-from lp.bugs.interfaces.bugtask import IBugTask
9 from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions
10 from lp.bugs.model.structuralsubscription import (
11 get_structural_subscriptions_for_bug,
12@@ -529,17 +528,19 @@
13 class BugPortletSubscribersWithDetails(LaunchpadView):
14 """A view that returns a JSON dump of the subscriber details for a bug."""
15
16- @property
17- def subscriber_data_js(self):
18- """Return subscriber_ids in a form suitable for JavaScript use."""
19+ @cachedproperty
20+ def api_request(self):
21+ return IWebServiceClientRequest(self.request)
22+
23+ def direct_subscriber_data(self, bug):
24+ """Get the direct subscriber data.
25+
26+ This method is isolated from the subscriber_data_js so that query
27+ count testing can be done accurately and robustly.
28+ """
29 data = []
30- if IBug.providedBy(self.context):
31- bug = self.context
32- elif IBugTask.providedBy(self.context):
33- bug = self.context.bug
34 details = list(bug.getDirectSubscribersWithDetails())
35- api_request = IWebServiceClientRequest(self.request)
36- for person, subscription in details:
37+ for person, subscribed_by, subscription in details:
38 can_edit = subscription.canBeUnsubscribedByUser(self.user)
39 if person == self.user or (person.private and not can_edit):
40 # Skip the current user viewing the page,
41@@ -550,9 +551,10 @@
42 'name': person.name,
43 'display_name': person.displayname,
44 'web_link': canonical_url(person, rootsite='mainsite'),
45- 'self_link': absoluteURL(person, api_request),
46+ 'self_link': absoluteURL(person, self.api_request),
47 'is_team': person.is_team,
48 'can_edit': can_edit,
49+ 'display_subscribed_by': subscription.display_subscribed_by,
50 }
51 record = {
52 'subscriber': subscriber,
53@@ -560,6 +562,13 @@
54 removeSecurityProxy(subscription.bug_notification_level)),
55 }
56 data.append(record)
57+ return data
58+
59+ @property
60+ def subscriber_data_js(self):
61+ """Return subscriber_ids in a form suitable for JavaScript use."""
62+ bug = IBug(self.context)
63+ data = self.direct_subscriber_data(bug)
64
65 others = list(bug.getIndirectSubscribers())
66 for person in others:
67@@ -570,7 +579,7 @@
68 'name': person.name,
69 'display_name': person.displayname,
70 'web_link': canonical_url(person, rootsite='mainsite'),
71- 'self_link': absoluteURL(person, api_request),
72+ 'self_link': absoluteURL(person, self.api_request),
73 'is_team': person.is_team,
74 'can_edit': False,
75 }
76
77=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
78--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-30 22:41:02 +0000
79+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-07-21 18:09:56 +0000
80@@ -1,4 +1,4 @@
81-# Copyright 2010 Canonical Ltd. This software is licensed under the
82+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
83 # GNU Affero General Public License version 3 (see the file LICENSE).
84
85 """Tests for BugSubscription views."""
86@@ -6,7 +6,8 @@
87 __metaclass__ = type
88
89 from simplejson import dumps
90-
91+from storm.store import Store
92+from testtools.matchers import Equals
93 from zope.component import getUtility
94 from zope.traversing.browser import absoluteURL
95
96@@ -23,8 +24,10 @@
97 from lp.registry.interfaces.person import IPersonSet
98 from lp.testing import (
99 person_logged_in,
100+ StormStatementRecorder,
101 TestCaseWithFactory,
102 )
103+from lp.testing.matchers import HasQueryCount
104 from lp.testing.sampledata import ADMIN_EMAIL
105 from lp.testing.views import create_initialized_view
106
107@@ -467,7 +470,7 @@
108 self.assertEqual(dumps([]), harness.view.subscriber_data_js)
109
110 def test_data_person_subscription(self):
111- # A subscriber_data_js returns JSON string of a list
112+ # subscriber_data_js returns JSON string of a list
113 # containing all subscriber information needed for
114 # subscribers_list.js subscribers loading.
115 bug = self._makeBugWithNoSubscribers()
116@@ -487,18 +490,69 @@
117 'can_edit': False,
118 'web_link': canonical_url(subscriber),
119 'self_link': absoluteURL(subscriber, api_request),
120- },
121- 'subscription_level': "Lifecycle",
122- }
123- self.assertEqual(
124- dumps([expected_result]), harness.view.subscriber_data_js)
125+ 'display_subscribed_by': 'Self-subscribed',
126+ },
127+ 'subscription_level': "Lifecycle",
128+ }
129+ self.assertEqual(
130+ dumps([expected_result]), harness.view.subscriber_data_js)
131+
132+ def test_data_person_subscription_other_subscriber(self):
133+ # Ensure subscriber_data_js does the correct thing when the person who
134+ # did the subscribing is someone else.
135+ bug = self._makeBugWithNoSubscribers()
136+ subscribed_by = self.factory.makePerson(
137+ name="someone", displayname='Someone')
138+ subscriber = self.factory.makePerson(
139+ name='user', displayname='Subscriber Name')
140+ with person_logged_in(subscriber):
141+ bug.subscribe(person=subscriber,
142+ subscribed_by=subscribed_by,
143+ level=BugNotificationLevel.LIFECYCLE)
144+ harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
145+ api_request = IWebServiceClientRequest(harness.request)
146+
147+ expected_result = {
148+ 'subscriber': {
149+ 'name': 'user',
150+ 'display_name': 'Subscriber Name',
151+ 'is_team': False,
152+ 'can_edit': False,
153+ 'web_link': canonical_url(subscriber),
154+ 'self_link': absoluteURL(subscriber, api_request),
155+ 'display_subscribed_by': 'Subscribed by Someone (someone)',
156+ },
157+ 'subscription_level': "Lifecycle",
158+ }
159+ self.assertEqual(
160+ dumps([expected_result]), harness.view.subscriber_data_js)
161+
162+ def test_data_person_subscription_other_subscriber_query_count(self):
163+ # All subscriber data should be retrieved with a single query.
164+ bug = self._makeBugWithNoSubscribers()
165+ subscribed_by = self.factory.makePerson(
166+ name="someone", displayname='Someone')
167+ subscriber = self.factory.makePerson(
168+ name='user', displayname='Subscriber Name')
169+ with person_logged_in(subscriber):
170+ bug.subscribe(person=subscriber,
171+ subscribed_by=subscribed_by,
172+ level=BugNotificationLevel.LIFECYCLE)
173+ harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
174+ # Invoke the view method, ignoring the results.
175+ Store.of(bug).invalidate()
176+ with StormStatementRecorder() as recorder:
177+ harness.view.direct_subscriber_data(bug)
178+ self.assertThat(recorder, HasQueryCount(Equals(1)))
179
180 def test_data_team_subscription(self):
181 # For a team subscription, subscriber_data_js has is_team set
182 # to true.
183 bug = self._makeBugWithNoSubscribers()
184+ teamowner = self.factory.makePerson(
185+ name="team-owner", displayname="Team Owner")
186 subscriber = self.factory.makeTeam(
187- name='team', displayname='Team Name')
188+ name='team', displayname='Team Name', owner=teamowner)
189 with person_logged_in(subscriber.teamowner):
190 bug.subscribe(subscriber, subscriber.teamowner,
191 level=BugNotificationLevel.LIFECYCLE)
192@@ -513,6 +567,8 @@
193 'can_edit': False,
194 'web_link': canonical_url(subscriber),
195 'self_link': absoluteURL(subscriber, api_request),
196+ 'display_subscribed_by': \
197+ 'Subscribed by Team Owner (team-owner)',
198 },
199 'subscription_level': "Lifecycle",
200 }
201@@ -523,8 +579,10 @@
202 # For a team subscription, subscriber_data_js has can_edit
203 # set to true for team owner.
204 bug = self._makeBugWithNoSubscribers()
205+ teamowner = self.factory.makePerson(
206+ name="team-owner", displayname="Team Owner")
207 subscriber = self.factory.makeTeam(
208- name='team', displayname='Team Name')
209+ name='team', displayname='Team Name', owner=teamowner)
210 with person_logged_in(subscriber.teamowner):
211 bug.subscribe(subscriber, subscriber.teamowner,
212 level=BugNotificationLevel.LIFECYCLE)
213@@ -540,6 +598,8 @@
214 'can_edit': True,
215 'web_link': canonical_url(subscriber),
216 'self_link': absoluteURL(subscriber, api_request),
217+ 'display_subscribed_by': \
218+ 'Subscribed by Team Owner (team-owner)',
219 },
220 'subscription_level': "Lifecycle",
221 }
222@@ -552,8 +612,11 @@
223 # set to true for team member.
224 bug = self._makeBugWithNoSubscribers()
225 member = self.factory.makePerson()
226+ teamowner = self.factory.makePerson(
227+ name="team-owner", displayname="Team Owner")
228 subscriber = self.factory.makeTeam(
229- name='team', displayname='Team Name', members=[member])
230+ name='team', displayname='Team Name', owner=teamowner,
231+ members=[member])
232 with person_logged_in(subscriber.teamowner):
233 bug.subscribe(subscriber, subscriber.teamowner,
234 level=BugNotificationLevel.LIFECYCLE)
235@@ -569,6 +632,8 @@
236 'can_edit': True,
237 'web_link': canonical_url(subscriber),
238 'self_link': absoluteURL(subscriber, api_request),
239+ 'display_subscribed_by': \
240+ 'Subscribed by Team Owner (team-owner)',
241 },
242 'subscription_level': "Lifecycle",
243 }
244@@ -598,6 +663,7 @@
245 'can_edit': True,
246 'web_link': canonical_url(subscriber),
247 'self_link': absoluteURL(subscriber, api_request),
248+ 'display_subscribed_by': 'Self-subscribed',
249 },
250 'subscription_level': "Lifecycle",
251 }
252@@ -615,7 +681,7 @@
253 subscriber = self.factory.makePerson(
254 name='user', displayname='Subscriber Name')
255 subscribed_by = self.factory.makePerson(
256- name='someone', displayname='Subscribed By Name')
257+ name='someone', displayname='Someone')
258 with person_logged_in(subscriber):
259 bug.subscribe(subscriber, subscribed_by,
260 level=BugNotificationLevel.LIFECYCLE)
261@@ -630,6 +696,7 @@
262 'can_edit': True,
263 'web_link': canonical_url(subscriber),
264 'self_link': absoluteURL(subscriber, api_request),
265+ 'display_subscribed_by': 'Subscribed by Someone (someone)',
266 },
267 'subscription_level': "Lifecycle",
268 }
269
270=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
271--- lib/lp/bugs/doc/bugsubscription.txt 2011-06-23 21:46:48 +0000
272+++ lib/lp/bugs/doc/bugsubscription.txt 2011-07-21 18:09:56 +0000
273@@ -888,14 +888,14 @@
274 >>> ff_bug.subscribe(lifeless, mark)
275 <lp.bugs.model.bugsubscription.BugSubscription ...>
276 >>> subscriptions = [
277- ... "%s (%s)" % (
278+ ... "%s: %s" % (
279 ... subscription.person.displayname,
280 ... subscription.display_subscribed_by)
281 ... for subscription in ff_bug.getDirectSubscriptions()]
282 >>> for subscription in sorted(subscriptions):
283 ... print subscription
284- Mark Shuttleworth (Self-subscribed)
285- Robert Collins (Subscribed by Mark Shuttleworth)
286+ Mark Shuttleworth: Self-subscribed
287+ Robert Collins: Subscribed by Mark Shuttleworth (mark)
288 >>> params = CreateBugParams(
289 ... title="one more dupe test bug",
290 ... comment="one more dupe test description",
291@@ -906,8 +906,8 @@
292 >>> dupe_ff_bug.subscribe(foobar, lifeless)
293 <lp.bugs.model.bugsubscription.BugSubscription ...>
294 >>> for subscription in ff_bug.getSubscriptionsFromDuplicates():
295- ... print '%s (%s)' % (
296+ ... print '%s: %s' % (
297 ... subscription.person.displayname,
298 ... subscription.display_duplicate_subscribed_by)
299- Scott James Remnant (Self-subscribed to bug ...)
300- Foo Bar (Subscribed to bug ... by Robert Collins)
301+ Scott James Remnant: Self-subscribed to bug ...
302+ Foo Bar: Subscribed to bug ... by Robert Collins (lifeless)
303
304=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
305--- lib/lp/bugs/javascript/subscribers_list.js 2011-07-20 13:35:24 +0000
306+++ lib/lp/bugs/javascript/subscribers_list.js 2011-07-21 18:09:56 +0000
307@@ -192,8 +192,9 @@
308 * "name": "foobar",
309 * "display_name": "Foo Bar",
310 * "can_edit": true/false,
311- * "is_team": false/false,
312- * "web_link": "https://launchpad.dev/~foobar"
313+ * "is_team": true/false,
314+ * "web_link": "https://launchpad.dev/~foobar",
315+ * "display_subscribed_by": "Matt Zimmerman (mdz)"
316 * },
317 * "subscription_level": "Details"},
318 * { "subscriber": ... }
319@@ -322,7 +323,6 @@
320 var subscriber = person.getAttrs();
321 this.subscribers_list.addSubscriber(subscriber, 'Discussion');
322 this.subscribers_list.indicateSubscriberActivity(subscriber);
323-
324 var loader = this;
325
326 function on_success() {
327@@ -687,7 +687,7 @@
328 *
329 * @method _createSubscriberNode
330 * @param subscriber {Object} Object containing `name`, `display_name`
331- * `web_link` and `is_team` attributes.
332+ * `web_link`, `is_team` and `display_subscribed_by` attributes.
333 * @return {Object} Node containing a subscriber link.
334 */
335 SubscribersList.prototype._createSubscriberNode = function(subscriber) {
336@@ -712,6 +712,9 @@
337 } else {
338 subscriber_text.addClass('person');
339 }
340+ if (Y.Lang.isString(subscriber.display_subscribed_by)) {
341+ subscriber_link.set('title', subscriber.display_subscribed_by);
342+ }
343 subscriber_link.appendChild(subscriber_text);
344
345 subscriber_node.appendChild(subscriber_link);
346@@ -728,7 +731,8 @@
347 *
348 * @method addSubscriber
349 * @param subscriber {Object} Object containing `name`, `display_name`
350- * `web_link` and `is_team` attributes describing the subscriber.
351+ * `web_link`, `is_team`, and `display_subscribed_by` attributes describing
352+ * the subscriber.
353 * @param level {String} Level of the subscription.
354 * @param config {Object} Object containing potential 'unsubscribe' callback
355 * in the `unsubscribe_callback` attribute.
356
357=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
358--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-07-20 13:35:24 +0000
359+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-07-21 18:09:56 +0000
360@@ -780,18 +780,39 @@
361 var subscriber = {
362 name: 'user',
363 display_name: 'User Name',
364- web_link: 'http://launchpad.net/~user'
365+ web_link: 'http://launchpad.net/~user',
366+ display_subscribed_by: 'Subscribed by Someone (someone)'
367 };
368 var node = subscribers_list._createSubscriberNode(subscriber);
369 Y.Assert.isTrue(node.hasClass('subscriber'));
370
371 var link = node.one('a');
372 Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));
373-
374+ Y.Assert.areEqual(
375+ 'Subscribed by Someone (someone)', link.get('title'));
376 var text = link.one('span');
377 Y.Assert.areEqual('User Name', text.get('text'));
378 Y.Assert.isTrue(text.hasClass('sprite'));
379 Y.Assert.isTrue(text.hasClass('person'));
380+
381+ },
382+
383+ test_createSubscriberNode_missing_display_subscribed_by: function() {
384+ // When passed a subscriber object with no 'display_subscribed_by'
385+ // attribute then the title is simply not set but shows up
386+ // as a null string.
387+ var subscribers_list = setUpSubscribersList(this.root);
388+ var subscriber = {
389+ name: 'user',
390+ display_name: 'User Name',
391+ web_link: 'http://launchpad.net/~user'
392+ };
393+ var node = subscribers_list._createSubscriberNode(subscriber);
394+ Y.Assert.isTrue(node.hasClass('subscriber'));
395+
396+ var link = node.one('a');
397+ Y.Assert.areEqual('http://launchpad.net/~user', link.get('href'));
398+ Y.Assert.areEqual('', link.get('title'));
399 },
400
401 test_createSubscriberNode_display_name_truncated: function() {
402@@ -827,7 +848,7 @@
403
404 test_addSubscriber: function() {
405 // When there is no subscriber in the subscriber list,
406- // new node is constructed and appropriate section is added.
407+ // a new node is constructed and the appropriate section is added.
408 var subscribers_list = setUpSubscribersList(this.root);
409 var node = subscribers_list.addSubscriber(
410 { name: 'user' }, 'Details');
411
412=== modified file 'lib/lp/bugs/model/bug.py'
413--- lib/lp/bugs/model/bug.py 2011-07-19 20:34:27 +0000
414+++ lib/lp/bugs/model/bug.py 2011-07-21 18:09:56 +0000
415@@ -939,10 +939,12 @@
416
417 def getDirectSubscribersWithDetails(self):
418 """See `IBug`."""
419+ SubscribedBy = ClassAlias(Person, name="subscribed_by")
420 results = Store.of(self).find(
421- (Person, BugSubscription),
422+ (Person, SubscribedBy, BugSubscription),
423 BugSubscription.person_id == Person.id,
424 BugSubscription.bug_id == self.id,
425+ BugSubscription.subscribed_by_id == SubscribedBy.id,
426 Not(In(BugSubscription.person_id,
427 Select(BugMute.person_id, BugMute.bug_id == self.id)))
428 ).order_by(Person.displayname)
429
430=== modified file 'lib/lp/bugs/model/bugsubscription.py'
431--- lib/lp/bugs/model/bugsubscription.py 2011-06-23 04:55:46 +0000
432+++ lib/lp/bugs/model/bugsubscription.py 2011-07-21 18:09:56 +0000
433@@ -62,10 +62,11 @@
434 @property
435 def display_subscribed_by(self):
436 """See `IBugSubscription`."""
437- if self.person == self.subscribed_by:
438+ if self.person_id == self.subscribed_by_id:
439 return u'Self-subscribed'
440 else:
441- return u'Subscribed by %s' % self.subscribed_by.displayname
442+ return u'Subscribed by %s (%s)' % (
443+ self.subscribed_by.displayname, self.subscribed_by.name)
444
445 @property
446 def display_duplicate_subscribed_by(self):
447@@ -73,8 +74,9 @@
448 if self.person == self.subscribed_by:
449 return u'Self-subscribed to bug %s' % (self.bug_id)
450 else:
451- return u'Subscribed to bug %s by %s' % (self.bug_id,
452- self.subscribed_by.displayname)
453+ return u'Subscribed to bug %s by %s (%s)' % (
454+ self.bug_id, self.subscribed_by.displayname,
455+ self.subscribed_by.name)
456
457 def canBeUnsubscribedByUser(self, user):
458 """See `IBugSubscription`."""
459
460=== modified file 'lib/lp/bugs/model/tests/test_bug.py'
461--- lib/lp/bugs/model/tests/test_bug.py 2011-07-19 14:42:52 +0000
462+++ lib/lp/bugs/model/tests/test_bug.py 2011-07-21 18:09:56 +0000
463@@ -205,9 +205,27 @@
464 set(subscribers), set(direct_subscribers),
465 "Subscribers did not match expected value.")
466
467- def test_get_direct_subscribers_with_details(self):
468- # getDirectSubscribersWithDetails() returns both
469- # Person and BugSubscription records in one go.
470+ def test_get_direct_subscribers_with_details_other_subscriber(self):
471+ # getDirectSubscribersWithDetails() returns
472+ # Person and BugSubscription records in one go as well as the
473+ # BugSubscription.subscribed_by person.
474+ bug = self.factory.makeBug()
475+ with person_logged_in(bug.owner):
476+ # Unsubscribe bug owner so it doesn't taint the result.
477+ bug.unsubscribe(bug.owner, bug.owner)
478+ subscriber = self.factory.makePerson()
479+ subscribee = self.factory.makePerson()
480+ with person_logged_in(subscriber):
481+ subscription = bug.subscribe(
482+ subscribee, subscriber, level=BugNotificationLevel.LIFECYCLE)
483+ self.assertContentEqual(
484+ [(subscribee, subscriber, subscription)],
485+ bug.getDirectSubscribersWithDetails())
486+
487+ def test_get_direct_subscribers_with_details_self_subscribed(self):
488+ # getDirectSubscribersWithDetails() returns
489+ # Person and BugSubscription records in one go as well as the
490+ # BugSubscription.subscribed_by person.
491 bug = self.factory.makeBug()
492 with person_logged_in(bug.owner):
493 # Unsubscribe bug owner so it doesn't taint the result.
494@@ -216,9 +234,8 @@
495 with person_logged_in(subscriber):
496 subscription = bug.subscribe(
497 subscriber, subscriber, level=BugNotificationLevel.LIFECYCLE)
498-
499 self.assertContentEqual(
500- [(subscriber, subscription)],
501+ [(subscriber, subscriber, subscription)],
502 bug.getDirectSubscribersWithDetails())
503
504 def test_get_direct_subscribers_with_details_mute_excludes(self):