Merge lp:~gmb/launchpad/subscribers-timeout-bug-487015 into lp:launchpad

Proposed by Graham Binns
Status: Merged
Approved by: Gavin Panella
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~gmb/launchpad/subscribers-timeout-bug-487015
Merge into: lp:launchpad
Diff against target: 395 lines (+188/-20)
7 files modified
lib/lp/bugs/browser/bug.py (+16/-1)
lib/lp/bugs/configure.zcml (+4/-1)
lib/lp/bugs/doc/bug.txt (+116/-16)
lib/lp/bugs/interfaces/bug.py (+18/-0)
lib/lp/bugs/model/bug.py (+32/-0)
lib/lp/bugs/templates/bug-portlet-subscribers-content.pt (+1/-1)
lib/lp/bugs/templates/bug-portlet-subscribers.pt (+1/-1)
To merge this branch: bzr merge lp:~gmb/launchpad/subscribers-timeout-bug-487015
Reviewer Review Type Date Requested Status
Gavin Panella (community) code Approve
Review via email: mp+15238@code.launchpad.net

Commit message

The Bug page should no longer time out due to iterating over large numbers of subscribers when generating CSS classes.

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

This branch fixes bug 487015 by adding methods to IBug that allow us to
check what kind of subscription (if any) a Person has to a bug using DB
queries rather than getting the list of subscribers and iterating over
it.

I've added three methods:

 - personIsDirectSubscriber(): returns True only if the Person has an
   explicit BugSubscription for the current bug.
 - personIsSubscribedToDuplicate(): returns True only if the Person has
   a BugSubscription to one of a bug's duplicates.
 - personIsAlsoNotifiedSubscriber(): returns True if the Person doesn't
   have a BugSubscription for a bug but will receive bugmail anyway
   (assignees, contacts and structural subscribers fall into this
   category).

Note that personIsAlsoNotifiedSubscriber() and personIsDirectSubscriber()
are mutually exclusive but personIsDirectSubscriber() and
personIsSubscribedToDuplicate() are not.

For personIsDirectSubscriber() and personIsSubscribedToDuplicate() I've
used Storm to create the necessary DB queries. However, I couldn't do
this for personIsAlsoNotifiedSubscriber() because "also notified
subscribers" come from a mish-mash of sources. I don't think this is a
problem (at least not right now) because there are usually less
"also notified" subscribers than there are subscribers from duplicates,
and the timeouts that raised this bug in the first place are due to the
number of duplicate subscribers and the amount of iteration we do over
that set.

I've updated BugViewMixin.subscription_class, which was where the
timeout problem identified in bug 487015 originated, to use the new
methods when determining what CSS class to return for a subscription.
Hopefully this will speed matters up somewhat.

I've also done a drive-by conversion of bug.txt to ReST.

NOTE: `make lint` shows a lot of "Operator is not preceded by a space"
errors, which look like crack to me and I'm not sure what to do about
them. Any ideas?

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/bug.py
  lib/lp/bugs/doc/bug.txt
  lib/lp/bugs/interfaces/bug.py
  lib/lp/bugs/model/bug.py

== Pylint notices ==

lib/lp/bugs/browser/bug.py
    28: [F0401] Unable to import 'email.MIMEMultipart' (No module named MIMEMultipart)
    29: [F0401] Unable to import 'email.MIMEText' (No module named MIMEText)
    43: [F0401] Unable to import 'lazr.enum' (No module named enum)
    44: [F0401] Unable to import 'lazr.lifecycle.event' (No module named lifecycle)
    45: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    46: [F0401] Unable to import 'lazr.restful.interfaces' (No module named restful)

lib/lp/bugs/interfaces/bug.py
    49: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    55: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    56: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    466: [C0322, IBug.addAttachment] Operator not preceded by a space
    comment=Text(), filename=TextLine(), is_patch=Bool(),
    ^
    co...

Read more...

Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (12.6 KiB)

Hi Graham,

I have some comments on how to make two of the new methods a bit
faster, but otherwise it all looks good.

I'm still concerned that it's doing at least one query for every
subscriber, rather than batching things up, but at least this branch
should reduce the overhead a lot... actually, I've suddenly had a
thought...

BugViewMixin.duplicate_subscribers and direct_subscribers are cached
properties, so the current implementation of subscription_class (if
subscribed_person in self.xxx_subscribers) is not inefficient. I'm now
worried that this branch may actually cause performance to degrade.

Gavin.

> === modified file 'lib/lp/bugs/browser/bug.py'
> --- lib/lp/bugs/browser/bug.py 2009-11-17 14:58:39 +0000
> +++ lib/lp/bugs/browser/bug.py 2009-11-25 11:02:35 +0000
> @@ -437,12 +437,15 @@
>
> For example, "subscribed-false dup-subscribed-true".
> """
> - if subscribed_person in self.duplicate_subscribers:
> + bug = self.context
> +
> + if (bug.personIsSubscribedToDuplicate(subscribed_person) or
> + bug.personIsAlsoNotifiedSubscriber(subscribed_person)):
> dup_class = 'dup-subscribed-true'

Now that we can distinguish between from-dupe and also-notified, there
should probably be another CSS class to represent those users, even if
it visually looks exactly the same as dup-subscribed-true. But don't
do it - I meant it as an observation - unless you've got very itchy
fingers and your keyboard has a scratching board attached.

> else:
> dup_class = 'dup-subscribed-false'
>
> - if subscribed_person in self.direct_subscribers:
> + if bug.personIsDirectSubscriber(subscribed_person):
> return 'subscribed-true %s' % dup_class
> else:
> return 'subscribed-false %s' % dup_class
>
> === modified file 'lib/lp/bugs/configure.zcml'
> --- lib/lp/bugs/configure.zcml 2009-11-20 04:21:24 +0000
> +++ lib/lp/bugs/configure.zcml 2009-11-25 11:02:35 +0000
> @@ -569,7 +569,10 @@
> is_complete
> who_made_private
> date_made_private
> - userCanView"/>
> + userCanView
> + personIsDirectSubscriber
> + personIsAlsoNotifiedSubscriber
> + personIsSubscribedToDuplicate"/>
> <require
> permission="launchpad.View"
> attributes="
>
> === modified file 'lib/lp/bugs/doc/bug.txt'
> --- lib/lp/bugs/doc/bug.txt 2009-10-26 17:11:03 +0000
> +++ lib/lp/bugs/doc/bug.txt 2009-11-25 11:02:35 +0000
> @@ -1,10 +1,12 @@
> -= Bugs in Malone =
> +Bugs in Malone
> +==============
>
> This document describes what a Bug is in Malone, and provides some (currently
> rather incomplete) info on how to poke at bugs through the Component
> Architecture.
>
> -== Working with Bugs ==
> +Working with Bugs
> +-----------------

I didn't know we were definitely moving over to reST, but I assume you
know better because I suspect I'm a bit behind on that front.

>
> Bugs are created and retrieved via IBugSet.
>
> @@ -68,7 +70,8 @@
> >>> print result_set.count(...

review: Needs Information (code)
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (11.7 KiB)

On Wed, Nov 25, 2009 at 12:07:21PM -0000, Gavin Panella wrote:
> Review: Needs Information code
> Hi Graham,
>
> I have some comments on how to make two of the new methods a bit
> faster, but otherwise it all looks good.
>
> I'm still concerned that it's doing at least one query for every
> subscriber, rather than batching things up, but at least this branch
> should reduce the overhead a lot... actually, I've suddenly had a
> thought...
>
> BugViewMixin.duplicate_subscribers and direct_subscribers are cached
> properties, so the current implementation of subscription_class (if
> subscribed_person in self.xxx_subscribers) is not inefficient. I'm now
> worried that this branch may actually cause performance to degrade.

Ah, so I'd partially misunderstood what subscription_class does and also
managed to miss a step.

The problem isn't that subscription_class is inefficient per se. It
isn't - for large result sets. However, for a single call it's
massively, massively inefficient, and when the bug page is loaded that's
how many times it gets called - once. No more. It's called when the
portlet contents are set up, but that happens *after* the bug page has
rendered, via AJAX, so that's not a concern. But that one call is
bringing the bug page to its knees.

So, I've added a new @property, current_user_subscription_class, which
uses the new methods to work on the subscription class for that one
call, and made the old subscription_class method into
getSubscriptionClassForUser (which it should have been called in the
first place; one of the things that confused me was that it was a PEP8
method name).

> Gavin.
>
>
> > === modified file 'lib/lp/bugs/browser/bug.py'
> > --- lib/lp/bugs/browser/bug.py 2009-11-17 14:58:39 +0000
> > +++ lib/lp/bugs/browser/bug.py 2009-11-25 11:02:35 +0000
> > @@ -437,12 +437,15 @@
> >
> > For example, "subscribed-false dup-subscribed-true".
> > """
> > - if subscribed_person in self.duplicate_subscribers:
> > + bug = self.context
> > +
> > + if (bug.personIsSubscribedToDuplicate(subscribed_person) or
> > + bug.personIsAlsoNotifiedSubscriber(subscribed_person)):
> > dup_class = 'dup-subscribed-true'
>
> Now that we can distinguish between from-dupe and also-notified, there
> should probably be another CSS class to represent those users, even if
> it visually looks exactly the same as dup-subscribed-true. But don't
> do it - I meant it as an observation - unless you've got very itchy
> fingers and your keyboard has a scratching board attached.
>

Nooooooo. And YAGNI, for the moment anyway.

> > === modified file 'lib/lp/bugs/doc/bug.txt'
> > --- lib/lp/bugs/doc/bug.txt 2009-10-26 17:11:03 +0000
> > +++ lib/lp/bugs/doc/bug.txt 2009-11-25 11:02:35 +0000
> > @@ -1,10 +1,12 @@
> > -= Bugs in Malone =
> > +Bugs in Malone
> > +==============
> >
> > This document describes what a Bug is in Malone, and provides some (currently
> > rather incomplete) info on how to poke at bugs through the Component
> > Architecture.
> >
> > -== Working with Bugs ==
> > +Working with Bugs
> > +-----------------
>
> I didn't know we were definitely moving over to reST, but I ...

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

Ah, thanks for the explanation.

> +
> + # Re-fetch the bug so that the fact that it's a duplicate definitely
> + # registers.
> >>> bug = getUtility(IBugSet).get(bug.id)

Although it saves little, out of curiosity I discovered that

  IStore(bug).flush()

also works.

Gavin.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2009-11-17 14:58:39 +0000
+++ lib/lp/bugs/browser/bug.py 2009-11-25 15:15:29 +0000
@@ -432,7 +432,7 @@
432 ids[sub.name] = 'subscriber-%s' % sub.id432 ids[sub.name] = 'subscriber-%s' % sub.id
433 return ids433 return ids
434434
435 def subscription_class(self, subscribed_person):435 def getSubscriptionClassForUser(self, subscribed_person):
436 """Return a set of CSS class names based on subscription status.436 """Return a set of CSS class names based on subscription status.
437437
438 For example, "subscribed-false dup-subscribed-true".438 For example, "subscribed-false dup-subscribed-true".
@@ -447,6 +447,21 @@
447 else:447 else:
448 return 'subscribed-false %s' % dup_class448 return 'subscribed-false %s' % dup_class
449449
450 @property
451 def current_user_subscription_class(self):
452 bug = self.context
453
454 if (bug.personIsSubscribedToDuplicate(self.user) or
455 bug.personIsAlsoNotifiedSubscriber(self.user)):
456 dup_class = 'dup-subscribed-true'
457 else:
458 dup_class = 'dup-subscribed-false'
459
460 if bug.personIsDirectSubscriber(self.user):
461 return 'subscribed-true %s' % dup_class
462 else:
463 return 'subscribed-false %s' % dup_class
464
450465
451class BugView(LaunchpadView, BugViewMixin):466class BugView(LaunchpadView, BugViewMixin):
452 """View class for presenting information about an `IBug`.467 """View class for presenting information about an `IBug`.
453468
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2009-11-20 04:21:24 +0000
+++ lib/lp/bugs/configure.zcml 2009-11-25 15:15:29 +0000
@@ -569,7 +569,10 @@
569 is_complete569 is_complete
570 who_made_private570 who_made_private
571 date_made_private571 date_made_private
572 userCanView"/>572 userCanView
573 personIsDirectSubscriber
574 personIsAlsoNotifiedSubscriber
575 personIsSubscribedToDuplicate"/>
573 <require576 <require
574 permission="launchpad.View"577 permission="launchpad.View"
575 attributes="578 attributes="
576579
=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt 2009-10-26 17:11:03 +0000
+++ lib/lp/bugs/doc/bug.txt 2009-11-25 15:15:29 +0000
@@ -1,10 +1,12 @@
1= Bugs in Malone =1Bugs in Malone
2==============
23
3This document describes what a Bug is in Malone, and provides some (currently4This document describes what a Bug is in Malone, and provides some (currently
4rather incomplete) info on how to poke at bugs through the Component5rather incomplete) info on how to poke at bugs through the Component
5Architecture.6Architecture.
67
7== Working with Bugs ==8Working with Bugs
9-----------------
810
9Bugs are created and retrieved via IBugSet.11Bugs are created and retrieved via IBugSet.
1012
@@ -68,7 +70,8 @@
68 >>> print result_set.count()70 >>> print result_set.count()
69 071 0
7072
71== Bug creation events ==73Bug creation events
74-------------------
7275
73IObjectCreatedEvent events are fired off when a bug is created. First76IObjectCreatedEvent events are fired off when a bug is created. First
74we will register a handler to observe the event.77we will register a handler to observe the event.
@@ -155,7 +158,8 @@
155 True158 True
156159
157160
158== Interface check ==161Interface check
162---------------
159163
160It is guaranteed to implement the correct interface, too:164It is guaranteed to implement the correct interface, too:
161165
@@ -166,7 +170,9 @@
166(We grab the object directly from the database here to avoid it being170(We grab the object directly from the database here to avoid it being
167security proxied, which doesn't make sense to test here.)171security proxied, which doesn't make sense to test here.)
168172
169== Searching for Bugs ==173
174Searching for Bugs
175------------------
170176
171To search for bugs matching specific criteria, use IBugSet.searchAsUser:177To search for bugs matching specific criteria, use IBugSet.searchAsUser:
172178
@@ -198,7 +204,9 @@
198204
199 >>> login(ANONYMOUS)205 >>> login(ANONYMOUS)
200206
201== Absolute URLs ==207
208Absolute URLs
209-------------
202210
203For things like bug notification emails, it's handy to be able to211For things like bug notification emails, it's handy to be able to
204include a URL to the bug inside the email.212include a URL to the bug inside the email.
@@ -207,7 +215,9 @@
207 >>> print canonical_url(firefox_crashes)215 >>> print canonical_url(firefox_crashes)
208 http://.../bugs/6216 http://.../bugs/6
209217
210== Bug Privacy ==218
219Bug Privacy
220-----------
211221
212A Bug has a "private" field. If Bug.private is False, the bug is222A Bug has a "private" field. If Bug.private is False, the bug is
213publicly visible. If Bug.private is True, only people who are directly223publicly visible. If Bug.private is True, only people who are directly
@@ -503,7 +513,8 @@
503 []513 []
504514
505515
506== Prevent reporter from being subscribed to filed bugs ==516Prevent reporter from being subscribed to filed bugs
517----------------------------------------------------
507518
508If necessary, subscriber_reporter may be specified when creating a bug,519If necessary, subscriber_reporter may be specified when creating a bug,
509to prevent the reporter from being subscribed to the bug. This is useful520to prevent the reporter from being subscribed to the bug. This is useful
@@ -517,7 +528,8 @@
517 []528 []
518529
519530
520== Date Last Updated ==531Date Last Updated
532-----------------
521533
522Malone tracks the last time a change was made to a534Malone tracks the last time a change was made to a
523bug. IBug.date_last_updated stores the date when anything is changed or535bug. IBug.date_last_updated stores the date when anything is changed or
@@ -956,7 +968,8 @@
956 True968 True
957969
958970
959== Bug Completeness ==971Bug Completeness
972----------------
960973
961A bug is considered "complete" iff all of its bugtasks are themselves974A bug is considered "complete" iff all of its bugtasks are themselves
962complete. The definition of completeness for a bugtask is that the bug975complete. The definition of completeness for a bugtask is that the bug
@@ -987,7 +1000,8 @@
987 mozilla-firefox (Debian) True1000 mozilla-firefox (Debian) True
9881001
9891002
990== Bug Tasks ==1003Bug Tasks
1004---------
9911005
992A bug can be targeted to more than one product, distribution, or source1006A bug can be targeted to more than one product, distribution, or source
993package. A BugTask is used to represent a target, which has its own1007package. A BugTask is used to represent a target, which has its own
@@ -1049,7 +1063,8 @@
1049 True1063 True
10501064
10511065
1052== Bug Expiration ==1066Bug Expiration
1067--------------
10531068
1054Incomplete bug reports may expire when they become inactive. Expiration1069Incomplete bug reports may expire when they become inactive. Expiration
1055is only available to projects that use Launchpad to track bugs. There1070is only available to projects that use Launchpad to track bugs. There
@@ -1128,7 +1143,8 @@
1128that can or cannot expire.1143that can or cannot expire.
11291144
11301145
1131== Bug Comments ==1146Bug Comments
1147------------
11321148
1133A bug comment is actually made up of a number of chunks. The1149A bug comment is actually made up of a number of chunks. The
1134IBug.getMessageChunks() method allows you to retreive these chunks in a1150IBug.getMessageChunks() method allows you to retreive these chunks in a
@@ -1166,7 +1182,8 @@
1166 2 Strange bug with duplicate messages. Bug #2 in Tomcat: "Blackhole Trash folder"1182 2 Strange bug with duplicate messages. Bug #2 in Tomcat: "Blackhole Trash folder"
11671183
11681184
1169== Affected users ==1185Affected users
1186--------------
11701187
1171Users can mark bugs as affecting or not affecting them. For each bug we1188Users can mark bugs as affecting or not affecting them. For each bug we
1172then keep a count of the number of users affected by it, as well as the1189then keep a count of the number of users affected by it, as well as the
@@ -1235,7 +1252,8 @@
1235 [<Person at ...>]1252 [<Person at ...>]
12361253
12371254
1238== Getting the distinct set of Bugs for a set of BugTasks ==1255Getting the distinct set of Bugs for a set of BugTasks
1256------------------------------------------------------
12391257
1240Sometimes we have a set of BugTasks for which we want to get only the1258Sometimes we have a set of BugTasks for which we want to get only the
1241distinct set of bugs, i.e. there are several BugTasks in our set which1259distinct set of bugs, i.e. there are several BugTasks in our set which
@@ -1339,7 +1357,8 @@
1339 New bug 01357 New bug 0
13401358
13411359
1342== Links to HWDB submissions ==1360Links to HWDB submissions
1361-------------------------
13431362
1344We can link a HWDB submission to a bug, indicating that the1363We can link a HWDB submission to a bug, indicating that the
1345submission contains information that could help developers1364submission contains information that could help developers
@@ -1389,3 +1408,84 @@
1389 >>> test_bug.unlinkHWSubmission(submission)1408 >>> test_bug.unlinkHWSubmission(submission)
1390 >>> print test_bug.getHWSubmissions().count()1409 >>> print test_bug.getHWSubmissions().count()
1391 01410 0
1411
1412
1413Discovering subscription types
1414------------------------------
1415
1416It's possible to find out how a person is subscribed to a bug by calling
1417the bug's personIsDirectSubscriber(), personIsAlsoNotifiedSubscriber() or
1418personIsSubscribedToDuplicate() methods.
1419
1420If a person isn't subscribed to a bug, all of these methods will return
1421False.
1422
1423 >>> person = factory.makePerson()
1424 >>> bug = factory.makeBug()
1425
1426 >>> bug.personIsDirectSubscriber(person)
1427 False
1428
1429 >>> bug.personIsSubscribedToDuplicate(person)
1430 False
1431
1432 >>> bug.personIsAlsoNotifiedSubscriber(person)
1433 False
1434
1435If our person subscribes to the bug they'll show up as a direct
1436subscriber.
1437
1438 >>> subscription = bug.subscribe(person, person)
1439 >>> bug.personIsDirectSubscriber(person)
1440 True
1441
1442 >>> bug.personIsSubscribedToDuplicate(person)
1443 False
1444
1445 >>> bug.personIsAlsoNotifiedSubscriber(person)
1446 False
1447
1448If the user subscribes to a duplicate of the bug,
1449personIsSubscribedToDuplicate() will return True.
1450
1451 >>> dupe = factory.makeBug()
1452 >>> subscription = dupe.subscribe(person, person)
1453
1454 >>> dupe.duplicateof = bug
1455
1456 # Re-fetch the bug so that the fact that it's a duplicate definitely
1457 # registers.
1458 >>> bug = getUtility(IBugSet).get(bug.id)
1459 >>> bug.personIsSubscribedToDuplicate(person)
1460 True
1461
1462personIsSubscribedToDuplicate() will return True regardless of
1463the result of personIsDirectSubscriber(). personIsAlsoNotifiedSubscriber()
1464will still return False.
1465
1466 >>> bug.personIsDirectSubscriber(person)
1467 True
1468
1469 >>> bug.personIsAlsoNotifiedSubscriber(person)
1470 False
1471
1472If the user is subscribed to the bug for a reason other than a direct
1473BugSubscription or a subscription to a duplicate bug,
1474personIsAlsoNotifiedSubscriber() will return True, for example if the
1475user is the assignee for one of the bug's BugTask.
1476
1477 >>> new_bug = factory.makeBug()
1478 >>> new_bug.default_bugtask.transitionToAssignee(person)
1479 >>> new_bug.personIsAlsoNotifiedSubscriber(person)
1480 True
1481
1482If the person subscribes directly to the bug,
1483personIsAlsoNotifiedSubscriber() will return False, since direct
1484subscriptions always override indirect ones.
1485
1486 >>> subscription = new_bug.subscribe(person, person)
1487 >>> new_bug.personIsAlsoNotifiedSubscriber(person)
1488 False
1489
1490 >>> new_bug.personIsDirectSubscriber(person)
1491 True
13921492
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py 2009-11-18 23:20:49 +0000
+++ lib/lp/bugs/interfaces/bug.py 2009-11-25 15:15:30 +0000
@@ -945,6 +945,24 @@
945 return Bugs.945 return Bugs.
946 """946 """
947947
948 def personIsDirectSubscriber(person):
949 """Return True if the person is a direct subscriber to this `IBug`.
950
951 Otherwise, return False.
952 """
953
954 def personIsAlsoNotifiedSubscriber(person):
955 """Return True if the person is an indirect subscriber to this `IBug`.
956
957 Otherwise, return False.
958 """
959
960 def personIsSubscribedToDuplicate(person):
961 """Return True if the person subscribed to a duplicate of this `IBug`.
962
963 Otherwise, return False.
964 """
965
948966
949class InvalidBugTargetType(Exception):967class InvalidBugTargetType(Exception):
950 """Bug target's type is not valid."""968 """Bug target's type is not valid."""
951969
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2009-10-26 17:00:08 +0000
+++ lib/lp/bugs/model/bug.py 2009-11-25 15:15:29 +0000
@@ -1389,6 +1389,38 @@
1389 """See `IBug`."""1389 """See `IBug`."""
1390 return getUtility(IHWSubmissionBugSet).submissionsForBug(self, user)1390 return getUtility(IHWSubmissionBugSet).submissionsForBug(self, user)
13911391
1392 def personIsDirectSubscriber(self, person):
1393 """See `IBug`."""
1394 store = Store.of(self)
1395 subscriptions = store.find(
1396 BugSubscription,
1397 BugSubscription.bug == self,
1398 BugSubscription.person == person)
1399
1400 return not subscriptions.is_empty()
1401
1402 def personIsAlsoNotifiedSubscriber(self, person):
1403 """See `IBug`."""
1404 # We have to use getAlsoNotifiedSubscribers() here and iterate
1405 # over what it returns because "also notified subscribers" is
1406 # actually a composite of bug contacts, structural subscribers
1407 # and assignees. As such, it's not possible to get them all with
1408 # one query.
1409 also_notified_subscribers = self.getAlsoNotifiedSubscribers()
1410
1411 return person in also_notified_subscribers
1412
1413 def personIsSubscribedToDuplicate(self, person):
1414 """See `IBug`."""
1415 store = Store.of(self)
1416 subscriptions_from_dupes = store.find(
1417 BugSubscription,
1418 Bug.duplicateof == self,
1419 BugSubscription.bugID == Bug.id,
1420 BugSubscription.person == person)
1421
1422 return not subscriptions_from_dupes.is_empty()
1423
13921424
1393class BugSet:1425class BugSet:
1394 """See BugSet."""1426 """See BugSet."""
13951427
=== modified file 'lib/lp/bugs/templates/bug-portlet-subscribers-content.pt'
--- lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-05 19:01:12 +0000
+++ lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-25 15:15:29 +0000
@@ -35,7 +35,7 @@
35 tal:attributes="35 tal:attributes="
36 title string:Unsubscribe ${subscription/person/fmt:displayname};36 title string:Unsubscribe ${subscription/person/fmt:displayname};
37 id string:unsubscribe-${subscription/css_name};37 id string:unsubscribe-${subscription/css_name};
38 class python: view.subscription_class(subscription.person)38 class python: view.getSubscriptionClassForUser(subscription.person)
39 "39 "
40 >40 >
41 <img class="unsub-icon" src="/@@/remove"41 <img class="unsub-icon" src="/@@/remove"
4242
=== modified file 'lib/lp/bugs/templates/bug-portlet-subscribers.pt'
--- lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-17 16:00:21 +0000
+++ lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-25 15:15:29 +0000
@@ -9,7 +9,7 @@
9 <div class="section" tal:define="context_menu context/menu:context"9 <div class="section" tal:define="context_menu context/menu:context"
10 metal:define-slot="heading">10 metal:define-slot="heading">
11 <div11 <div
12 tal:attributes="class python: view.subscription_class(view.user)"12 tal:attributes="class view/current_user_subscription_class"
13 tal:content="structure context_menu/subscription/render" />13 tal:content="structure context_menu/subscription/render" />
14 <div id="sub-unsub-spinner">Subscribing...</div>14 <div id="sub-unsub-spinner">Subscribing...</div>
15 <div tal:content="structure context_menu/addsubscriber/render" />15 <div tal:content="structure context_menu/addsubscriber/render" />