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
1=== modified file 'lib/lp/bugs/browser/bug.py'
2--- lib/lp/bugs/browser/bug.py 2009-11-17 14:58:39 +0000
3+++ lib/lp/bugs/browser/bug.py 2009-11-25 15:15:29 +0000
4@@ -432,7 +432,7 @@
5 ids[sub.name] = 'subscriber-%s' % sub.id
6 return ids
7
8- def subscription_class(self, subscribed_person):
9+ def getSubscriptionClassForUser(self, subscribed_person):
10 """Return a set of CSS class names based on subscription status.
11
12 For example, "subscribed-false dup-subscribed-true".
13@@ -447,6 +447,21 @@
14 else:
15 return 'subscribed-false %s' % dup_class
16
17+ @property
18+ def current_user_subscription_class(self):
19+ bug = self.context
20+
21+ if (bug.personIsSubscribedToDuplicate(self.user) or
22+ bug.personIsAlsoNotifiedSubscriber(self.user)):
23+ dup_class = 'dup-subscribed-true'
24+ else:
25+ dup_class = 'dup-subscribed-false'
26+
27+ if bug.personIsDirectSubscriber(self.user):
28+ return 'subscribed-true %s' % dup_class
29+ else:
30+ return 'subscribed-false %s' % dup_class
31+
32
33 class BugView(LaunchpadView, BugViewMixin):
34 """View class for presenting information about an `IBug`.
35
36=== modified file 'lib/lp/bugs/configure.zcml'
37--- lib/lp/bugs/configure.zcml 2009-11-20 04:21:24 +0000
38+++ lib/lp/bugs/configure.zcml 2009-11-25 15:15:29 +0000
39@@ -569,7 +569,10 @@
40 is_complete
41 who_made_private
42 date_made_private
43- userCanView"/>
44+ userCanView
45+ personIsDirectSubscriber
46+ personIsAlsoNotifiedSubscriber
47+ personIsSubscribedToDuplicate"/>
48 <require
49 permission="launchpad.View"
50 attributes="
51
52=== modified file 'lib/lp/bugs/doc/bug.txt'
53--- lib/lp/bugs/doc/bug.txt 2009-10-26 17:11:03 +0000
54+++ lib/lp/bugs/doc/bug.txt 2009-11-25 15:15:29 +0000
55@@ -1,10 +1,12 @@
56-= Bugs in Malone =
57+Bugs in Malone
58+==============
59
60 This document describes what a Bug is in Malone, and provides some (currently
61 rather incomplete) info on how to poke at bugs through the Component
62 Architecture.
63
64-== Working with Bugs ==
65+Working with Bugs
66+-----------------
67
68 Bugs are created and retrieved via IBugSet.
69
70@@ -68,7 +70,8 @@
71 >>> print result_set.count()
72 0
73
74-== Bug creation events ==
75+Bug creation events
76+-------------------
77
78 IObjectCreatedEvent events are fired off when a bug is created. First
79 we will register a handler to observe the event.
80@@ -155,7 +158,8 @@
81 True
82
83
84-== Interface check ==
85+Interface check
86+---------------
87
88 It is guaranteed to implement the correct interface, too:
89
90@@ -166,7 +170,9 @@
91 (We grab the object directly from the database here to avoid it being
92 security proxied, which doesn't make sense to test here.)
93
94-== Searching for Bugs ==
95+
96+Searching for Bugs
97+------------------
98
99 To search for bugs matching specific criteria, use IBugSet.searchAsUser:
100
101@@ -198,7 +204,9 @@
102
103 >>> login(ANONYMOUS)
104
105-== Absolute URLs ==
106+
107+Absolute URLs
108+-------------
109
110 For things like bug notification emails, it's handy to be able to
111 include a URL to the bug inside the email.
112@@ -207,7 +215,9 @@
113 >>> print canonical_url(firefox_crashes)
114 http://.../bugs/6
115
116-== Bug Privacy ==
117+
118+Bug Privacy
119+-----------
120
121 A Bug has a "private" field. If Bug.private is False, the bug is
122 publicly visible. If Bug.private is True, only people who are directly
123@@ -503,7 +513,8 @@
124 []
125
126
127-== Prevent reporter from being subscribed to filed bugs ==
128+Prevent reporter from being subscribed to filed bugs
129+----------------------------------------------------
130
131 If necessary, subscriber_reporter may be specified when creating a bug,
132 to prevent the reporter from being subscribed to the bug. This is useful
133@@ -517,7 +528,8 @@
134 []
135
136
137-== Date Last Updated ==
138+Date Last Updated
139+-----------------
140
141 Malone tracks the last time a change was made to a
142 bug. IBug.date_last_updated stores the date when anything is changed or
143@@ -956,7 +968,8 @@
144 True
145
146
147-== Bug Completeness ==
148+Bug Completeness
149+----------------
150
151 A bug is considered "complete" iff all of its bugtasks are themselves
152 complete. The definition of completeness for a bugtask is that the bug
153@@ -987,7 +1000,8 @@
154 mozilla-firefox (Debian) True
155
156
157-== Bug Tasks ==
158+Bug Tasks
159+---------
160
161 A bug can be targeted to more than one product, distribution, or source
162 package. A BugTask is used to represent a target, which has its own
163@@ -1049,7 +1063,8 @@
164 True
165
166
167-== Bug Expiration ==
168+Bug Expiration
169+--------------
170
171 Incomplete bug reports may expire when they become inactive. Expiration
172 is only available to projects that use Launchpad to track bugs. There
173@@ -1128,7 +1143,8 @@
174 that can or cannot expire.
175
176
177-== Bug Comments ==
178+Bug Comments
179+------------
180
181 A bug comment is actually made up of a number of chunks. The
182 IBug.getMessageChunks() method allows you to retreive these chunks in a
183@@ -1166,7 +1182,8 @@
184 2 Strange bug with duplicate messages. Bug #2 in Tomcat: "Blackhole Trash folder"
185
186
187-== Affected users ==
188+Affected users
189+--------------
190
191 Users can mark bugs as affecting or not affecting them. For each bug we
192 then keep a count of the number of users affected by it, as well as the
193@@ -1235,7 +1252,8 @@
194 [<Person at ...>]
195
196
197-== Getting the distinct set of Bugs for a set of BugTasks ==
198+Getting the distinct set of Bugs for a set of BugTasks
199+------------------------------------------------------
200
201 Sometimes we have a set of BugTasks for which we want to get only the
202 distinct set of bugs, i.e. there are several BugTasks in our set which
203@@ -1339,7 +1357,8 @@
204 New bug 0
205
206
207-== Links to HWDB submissions ==
208+Links to HWDB submissions
209+-------------------------
210
211 We can link a HWDB submission to a bug, indicating that the
212 submission contains information that could help developers
213@@ -1389,3 +1408,84 @@
214 >>> test_bug.unlinkHWSubmission(submission)
215 >>> print test_bug.getHWSubmissions().count()
216 0
217+
218+
219+Discovering subscription types
220+------------------------------
221+
222+It's possible to find out how a person is subscribed to a bug by calling
223+the bug's personIsDirectSubscriber(), personIsAlsoNotifiedSubscriber() or
224+personIsSubscribedToDuplicate() methods.
225+
226+If a person isn't subscribed to a bug, all of these methods will return
227+False.
228+
229+ >>> person = factory.makePerson()
230+ >>> bug = factory.makeBug()
231+
232+ >>> bug.personIsDirectSubscriber(person)
233+ False
234+
235+ >>> bug.personIsSubscribedToDuplicate(person)
236+ False
237+
238+ >>> bug.personIsAlsoNotifiedSubscriber(person)
239+ False
240+
241+If our person subscribes to the bug they'll show up as a direct
242+subscriber.
243+
244+ >>> subscription = bug.subscribe(person, person)
245+ >>> bug.personIsDirectSubscriber(person)
246+ True
247+
248+ >>> bug.personIsSubscribedToDuplicate(person)
249+ False
250+
251+ >>> bug.personIsAlsoNotifiedSubscriber(person)
252+ False
253+
254+If the user subscribes to a duplicate of the bug,
255+personIsSubscribedToDuplicate() will return True.
256+
257+ >>> dupe = factory.makeBug()
258+ >>> subscription = dupe.subscribe(person, person)
259+
260+ >>> dupe.duplicateof = bug
261+
262+ # Re-fetch the bug so that the fact that it's a duplicate definitely
263+ # registers.
264+ >>> bug = getUtility(IBugSet).get(bug.id)
265+ >>> bug.personIsSubscribedToDuplicate(person)
266+ True
267+
268+personIsSubscribedToDuplicate() will return True regardless of
269+the result of personIsDirectSubscriber(). personIsAlsoNotifiedSubscriber()
270+will still return False.
271+
272+ >>> bug.personIsDirectSubscriber(person)
273+ True
274+
275+ >>> bug.personIsAlsoNotifiedSubscriber(person)
276+ False
277+
278+If the user is subscribed to the bug for a reason other than a direct
279+BugSubscription or a subscription to a duplicate bug,
280+personIsAlsoNotifiedSubscriber() will return True, for example if the
281+user is the assignee for one of the bug's BugTask.
282+
283+ >>> new_bug = factory.makeBug()
284+ >>> new_bug.default_bugtask.transitionToAssignee(person)
285+ >>> new_bug.personIsAlsoNotifiedSubscriber(person)
286+ True
287+
288+If the person subscribes directly to the bug,
289+personIsAlsoNotifiedSubscriber() will return False, since direct
290+subscriptions always override indirect ones.
291+
292+ >>> subscription = new_bug.subscribe(person, person)
293+ >>> new_bug.personIsAlsoNotifiedSubscriber(person)
294+ False
295+
296+ >>> new_bug.personIsDirectSubscriber(person)
297+ True
298
299=== modified file 'lib/lp/bugs/interfaces/bug.py'
300--- lib/lp/bugs/interfaces/bug.py 2009-11-18 23:20:49 +0000
301+++ lib/lp/bugs/interfaces/bug.py 2009-11-25 15:15:30 +0000
302@@ -945,6 +945,24 @@
303 return Bugs.
304 """
305
306+ def personIsDirectSubscriber(person):
307+ """Return True if the person is a direct subscriber to this `IBug`.
308+
309+ Otherwise, return False.
310+ """
311+
312+ def personIsAlsoNotifiedSubscriber(person):
313+ """Return True if the person is an indirect subscriber to this `IBug`.
314+
315+ Otherwise, return False.
316+ """
317+
318+ def personIsSubscribedToDuplicate(person):
319+ """Return True if the person subscribed to a duplicate of this `IBug`.
320+
321+ Otherwise, return False.
322+ """
323+
324
325 class InvalidBugTargetType(Exception):
326 """Bug target's type is not valid."""
327
328=== modified file 'lib/lp/bugs/model/bug.py'
329--- lib/lp/bugs/model/bug.py 2009-10-26 17:00:08 +0000
330+++ lib/lp/bugs/model/bug.py 2009-11-25 15:15:29 +0000
331@@ -1389,6 +1389,38 @@
332 """See `IBug`."""
333 return getUtility(IHWSubmissionBugSet).submissionsForBug(self, user)
334
335+ def personIsDirectSubscriber(self, person):
336+ """See `IBug`."""
337+ store = Store.of(self)
338+ subscriptions = store.find(
339+ BugSubscription,
340+ BugSubscription.bug == self,
341+ BugSubscription.person == person)
342+
343+ return not subscriptions.is_empty()
344+
345+ def personIsAlsoNotifiedSubscriber(self, person):
346+ """See `IBug`."""
347+ # We have to use getAlsoNotifiedSubscribers() here and iterate
348+ # over what it returns because "also notified subscribers" is
349+ # actually a composite of bug contacts, structural subscribers
350+ # and assignees. As such, it's not possible to get them all with
351+ # one query.
352+ also_notified_subscribers = self.getAlsoNotifiedSubscribers()
353+
354+ return person in also_notified_subscribers
355+
356+ def personIsSubscribedToDuplicate(self, person):
357+ """See `IBug`."""
358+ store = Store.of(self)
359+ subscriptions_from_dupes = store.find(
360+ BugSubscription,
361+ Bug.duplicateof == self,
362+ BugSubscription.bugID == Bug.id,
363+ BugSubscription.person == person)
364+
365+ return not subscriptions_from_dupes.is_empty()
366+
367
368 class BugSet:
369 """See BugSet."""
370
371=== modified file 'lib/lp/bugs/templates/bug-portlet-subscribers-content.pt'
372--- lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-05 19:01:12 +0000
373+++ lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-25 15:15:29 +0000
374@@ -35,7 +35,7 @@
375 tal:attributes="
376 title string:Unsubscribe ${subscription/person/fmt:displayname};
377 id string:unsubscribe-${subscription/css_name};
378- class python: view.subscription_class(subscription.person)
379+ class python: view.getSubscriptionClassForUser(subscription.person)
380 "
381 >
382 <img class="unsub-icon" src="/@@/remove"
383
384=== modified file 'lib/lp/bugs/templates/bug-portlet-subscribers.pt'
385--- lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-17 16:00:21 +0000
386+++ lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-25 15:15:29 +0000
387@@ -9,7 +9,7 @@
388 <div class="section" tal:define="context_menu context/menu:context"
389 metal:define-slot="heading">
390 <div
391- tal:attributes="class python: view.subscription_class(view.user)"
392+ tal:attributes="class view/current_user_subscription_class"
393 tal:content="structure context_menu/subscription/render" />
394 <div id="sub-unsub-spinner">Subscribing...</div>
395 <div tal:content="structure context_menu/addsubscriber/render" />