Merge lp:~bac/launchpad/accordion-client-2 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 12700
Proposed branch: lp:~bac/launchpad/accordion-client-2
Merge into: lp:launchpad
Diff against target: 2175 lines (+1097/-197)
29 files modified
lib/lp/bugs/browser/bugsubscription.py (+7/-2)
lib/lp/bugs/browser/bugtarget.py (+10/-3)
lib/lp/bugs/browser/bugtask.py (+9/-9)
lib/lp/bugs/browser/structuralsubscription.py (+36/-31)
lib/lp/bugs/templates/bug-subscription-list.pt (+4/-2)
lib/lp/bugs/templates/buglisting-default.pt (+13/-0)
lib/lp/bugs/templates/bugtarget-bugs.pt (+17/-2)
lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt (+39/-14)
lib/lp/bugs/templates/bugtarget-subscription-list.pt (+4/-2)
lib/lp/registry/browser/__init__.py (+12/-0)
lib/lp/registry/browser/distribution.py (+34/-11)
lib/lp/registry/browser/distributionsourcepackage.py (+28/-10)
lib/lp/registry/browser/distroseries.py (+34/-10)
lib/lp/registry/browser/milestone.py (+14/-2)
lib/lp/registry/browser/product.py (+23/-32)
lib/lp/registry/browser/productseries.py (+30/-18)
lib/lp/registry/browser/project.py (+28/-16)
lib/lp/registry/browser/tests/test_product.py (+6/-2)
lib/lp/registry/browser/tests/test_subscription_links.py (+609/-0)
lib/lp/registry/javascript/structural-subscription.js (+1/-1)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+17/-17)
lib/lp/registry/templates/distribution-index.pt (+19/-0)
lib/lp/registry/templates/distributionsourcepackage-index.pt (+14/-1)
lib/lp/registry/templates/distroseries-index.pt (+20/-2)
lib/lp/registry/templates/milestone-index.pt (+15/-2)
lib/lp/registry/templates/product-index.pt (+2/-2)
lib/lp/registry/templates/productseries-index.pt (+29/-6)
lib/lp/registry/templates/project-index.pt (+19/-0)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~bac/launchpad/accordion-client-2
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+55361@code.launchpad.net

Commit message

[r=benji][no-qa] Add new JS-driven structural subscription links, guarded by feature flag, to IStructuralSubscriptionTarget overview and bugs pages.

Description of the change

= Summary =

The new links to subscribe to structural subscriptions are placed on the
overview and bugs facet pages for all IStructuralSubscriptionTargets.
The old structural subscriptions had such a link only on the bugs facet
for those targets.

The new menu link called 'subscribe_to_bug_mail' replaced the older
'subscribe' menu link. It is done conditionally on the feature flag.

I apologize for the overly large size of this branch.

== Proposed fix ==

The link is conditionally added to the various browser view code based
on the feature flag. The corresponding page templates are updated to
include the required JavaScript and the target for the overlay to hook
into the DOM.

For well-behaved pages, simply replacing the old link with the new on in
the navigation menu is enough to have the links rendered properly. (The
links are initially hidden and then activated by the JavaScript if it is
enabled on a supported browser.) The rendering is handled by the
'+global-actions' menu generation macro.

Some pages, however, don't use that mechanism and create the links
manually. The existing pattern was followed. For some of those, the
portlet's visibility is controlled by a condition based on the new link
being enabled, since it uses 'launchpad.AnyPerson' which is the least
restrictive permission of any of the items in the portlet. That part is
pretty gross and suggestions for a cleaner approach would be welcome.

== Pre-implementation notes ==

Chats and contributions from almost everyone on the Yellow Squad.

== Implementation details ==

As above.

== Tests ==

bin/test -vvm lp.registry -t test_subscription_links

== Demo and Q/A ==

Go to

https://launchpad.dev/firefox
https://bugs.launchpad.dev/firefox

https://launchpad.dev/ubuntu
https://bugs.launchpad.dev/ubuntu

Note that distributions have special rules as to whether a user can
subscribe. If a bug supervisor is set, then only members of the bug
supervisor team can subscribe themselves. An admin can also subscribe
himself. NOTE: we must ensure that a non-admin member of the bug
supervisor team is not allowed to subscribe others.

In order to turn the feature flag to to
https://launchpad.dev/firefox/+feature-rules

To turn it on enter:

malone.advanced-structural-subscriptions.enabled default 1 on

To turn the flag off enter:

malone.advanced-structural-subscriptions.enabled default 1

Note you *must* include a trailing space at the end of the line.

= Launchpad lint =

All of the following lint items are false positives.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt
  lib/lp/registry/templates/product-index.pt
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/registry/templates/milestone-index.pt
  lib/lp/registry/templates/distroseries-index.pt
  lib/lp/registry/templates/distribution-index.pt
  lib/lp/registry/templates/distributionsourcepackage-index.pt
  lib/lp/bugs/browser/structuralsubscription.py
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/distributionsourcepackage.py
  lib/lp/registry/browser/productseries.py
  lib/lp/bugs/templates/buglisting-default.pt
  lib/lp/bugs/browser/bugsubscription.py
  lib/lp/registry/browser/tests/test_product.py
  lib/lp/services/features/flags.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/templates/bug-subscription-list.pt
  lib/lp/bugs/templates/bugtarget-bugs.pt
  lib/lp/bugs/templates/bugtarget-subscription-list.pt
  lib/lp/registry/templates/project-index.pt
  lib/lp/registry/javascript/structural-subscription.js
  lib/lp/registry/browser/distribution.py
  lib/lp/registry/browser/distroseries.py
  lib/lp/registry/templates/productseries-index.pt
  lib/lp/registry/browser/milestone.py
  lib/lp/registry/browser/project.py
  lib/lp/registry/browser/tests/test_subscription_links.py

./lib/lp/bugs/templates/bugtarget-bugs.pt
     169: not well-formed (invalid token)
./lib/lp/registry/browser/tests/test_subscription_links.py
     295: E301 expected 1 blank line, found 2
     387: E301 expected 1 blank line, found 2
     481: E301 expected 1 blank line, found 2
     567: E301 expected 1 blank line, found 2

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. I just have a few small comments.

I like the substitution of a function for the mix-in.

> ./lib/lp/bugs/templates/bugtarget-bugs.pt
> 169: not well-formed (invalid token)

The above can be fixed by replacing the ampersands with "&".

> ./lib/lp/registry/browser/tests/test_subscription_links.py
> 295: E301 expected 1 blank line, found 2
> 387: E301 expected 1 blank line, found 2
> 481: E301 expected 1 blank line, found 2
> 567: E301 expected 1 blank line, found 2

These are indeed spurious, but are caused by the commented-out
functions. Looking at the functions it seems that they should be
removed or made to work. I tried a little to get them to work without
luck.

The DistributionSourcePackageActionMenu class now has a mutable class
attribute ("links"), which of course is normally a bad thing but it
doesn't appear that it is ever accessed because there is a property of
the same name. I suggest just removing the class attribute altogether.

The snippet

    use_advanced_features = getFeatureFlag(
        'malone.advanced-structural-subscriptions.enabled')
    if use_advanced_features:
        links.append('subscribe_to_bug_mail')
    else:
        links.append('subscribe')

is repeated so often I wonder if a helper function named something like
"add_subscribe_link" might not be DRYer.

In these files:

    lib/lp/bugs/templates/bug-subscription-list.pt
    lib/lp/bugs/templates/bugtarget-bugs.pt
    lib/lp/bugs/templates/bugtarget-subscription-list.pt
    lib/lp/registry/templates/distribution-index.pt
    lib/lp/registry/templates/distributionsourcepackage-index.pt
    lib/lp/registry/templates/distroseries-index.pt
    lib/lp/registry/templates/milestone-index.pt
    lib/lp/registry/templates/product-index.pt
    lib/lp/registry/templates/productseries-index.pt

I think you want a "var" on the line:

    module = Y.lp.registry.structural_subscription;

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

Thanks for the review and good suggestions Benji. I have incorporated all of them. Getting the anonymous tests to work was easy using the somewhat obscure 'no_login' parameter.

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-03-29 00:11:57 +0000
3+++ lib/lp/bugs/browser/bugsubscription.py 2011-03-29 22:36:23 +0000
4@@ -40,7 +40,7 @@
5 )
6 from lp.bugs.browser.bug import BugViewMixin
7 from lp.bugs.browser.structuralsubscription import (
8- StructuralSubscriptionJSMixin,
9+ expose_structural_subscription_data_to_js,
10 )
11 from lp.bugs.enum import BugNotificationLevel, HIDDEN_BUG_NOTIFICATION_LEVELS
12 from lp.bugs.interfaces.bugsubscription import IBugSubscription
13@@ -578,9 +578,14 @@
14 return 'subscriber-%s' % self.subscription.person.id
15
16
17-class BugSubscriptionListView(StructuralSubscriptionJSMixin, LaunchpadView):
18+class BugSubscriptionListView(LaunchpadView):
19 """A view to show all a person's subscriptions to a bug."""
20
21+ def initialize(self):
22+ super(BugSubscriptionListView, self).initialize()
23+ expose_structural_subscription_data_to_js(
24+ self.context, self.request, self.user, self.subscriptions)
25+
26 @property
27 def subscriptions(self):
28 return get_structural_subscriptions_for_bug(
29
30=== modified file 'lib/lp/bugs/browser/bugtarget.py'
31--- lib/lp/bugs/browser/bugtarget.py 2011-03-23 19:20:03 +0000
32+++ lib/lp/bugs/browser/bugtarget.py 2011-03-29 22:36:23 +0000
33@@ -99,7 +99,7 @@
34 from lp.bugs.browser.bugrole import BugRoleMixin
35 from lp.bugs.browser.bugtask import BugTaskSearchListingView
36 from lp.bugs.browser.structuralsubscription import (
37- StructuralSubscriptionJSMixin,
38+ expose_structural_subscription_data_to_js,
39 )
40 from lp.bugs.browser.widgets.bug import (
41 BugTagsWidget,
42@@ -1308,10 +1308,12 @@
43 return 'Bugs in %s' % self.context.title
44
45 def initialize(self):
46- BugTaskSearchListingView.initialize(self)
47+ super(BugTargetBugsView, self).initialize()
48 bug_statuses_to_show = list(UNRESOLVED_BUGTASK_STATUSES)
49 if IDistroSeries.providedBy(self.context):
50 bug_statuses_to_show.append(BugTaskStatus.FIXRELEASED)
51+ expose_structural_subscription_data_to_js(
52+ self.context, self.request, self.user)
53
54 @property
55 def can_have_external_bugtracker(self):
56@@ -1565,9 +1567,14 @@
57 return ProxiedLibraryFileAlias(patch.libraryfile, patch).http_url
58
59
60-class TargetSubscriptionView(StructuralSubscriptionJSMixin, LaunchpadView):
61+class TargetSubscriptionView(LaunchpadView):
62 """A view to show all a person's structural subscriptions to a target."""
63
64+ def initialize(self):
65+ super(TargetSubscriptionView, self).initialize()
66+ expose_structural_subscription_data_to_js(
67+ self.context, self.request, self.user, self.subscriptions)
68+
69 @property
70 def subscriptions(self):
71 return get_structural_subscriptions_for_target(
72
73=== modified file 'lib/lp/bugs/browser/bugtask.py'
74--- lib/lp/bugs/browser/bugtask.py 2011-03-25 15:33:51 +0000
75+++ lib/lp/bugs/browser/bugtask.py 2011-03-29 22:36:23 +0000
76@@ -195,6 +195,9 @@
77 BugTextView,
78 BugViewMixin,
79 )
80+from lp.bugs.browser.structuralsubscription import (
81+ expose_structural_subscription_data_to_js,
82+ )
83 from lp.bugs.browser.bugcomment import (
84 build_comments_from_chunks,
85 group_comments_with_activity,
86@@ -275,12 +278,11 @@
87 from lp.services.fields import PersonChoice
88 from lp.services.propertycache import (
89 cachedproperty,
90- get_property_cache,
91 )
92
93
94 DISPLAY_BUG_STATUS_FOR_PATCHES = {
95- BugTaskStatus.NEW: True,
96+ BugTaskStatus.NEW: True,
97 BugTaskStatus.INCOMPLETE: True,
98 BugTaskStatus.INVALID: False,
99 BugTaskStatus.WONTFIX: False,
100@@ -290,7 +292,7 @@
101 BugTaskStatus.FIXCOMMITTED: True,
102 BugTaskStatus.FIXRELEASED: False,
103 BugTaskStatus.UNKNOWN: False,
104- BugTaskStatus.EXPIRED: False
105+ BugTaskStatus.EXPIRED: False,
106 }
107
108
109@@ -2205,11 +2207,6 @@
110 return Link(
111 '+securitycontact', 'Change security contact', icon='edit')
112
113- def subscribe(self):
114- user = getUtility(ILaunchBag).user
115- if self.context.userCanAlterBugSubscription(user):
116- return Link('+subscribe', 'Subscribe to bug mail', icon='edit')
117-
118 def nominations(self):
119 return Link('+nominations', 'Review nominations', icon='bug')
120
121@@ -2346,6 +2343,9 @@
122 # needing validation is already available internally to self.
123 self._validate(None, {})
124
125+ expose_structural_subscription_data_to_js(
126+ self.context, self.request, self.user)
127+
128 @property
129 def columns_to_show(self):
130 """Returns a sequence of column names to be shown in the listing."""
131@@ -3196,7 +3196,7 @@
132 # Hint to optimize when there are many bugtasks.
133 view.many_bugtasks = self.many_bugtasks
134 return view
135-
136+
137 def getBugTaskAndNominationViews(self):
138 """Return the IBugTasks and IBugNominations views for this bug.
139
140
141=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
142--- lib/lp/bugs/browser/structuralsubscription.py 2011-03-24 15:31:53 +0000
143+++ lib/lp/bugs/browser/structuralsubscription.py 2011-03-29 22:36:23 +0000
144@@ -5,9 +5,9 @@
145
146 __all__ = [
147 'expose_enum_to_js',
148+ 'expose_structural_subscription_data_to_js',
149 'expose_user_administered_teams_to_js',
150 'expose_user_subscriptions_to_js',
151- 'StructuralSubscriptionJSMixin',
152 'StructuralSubscriptionMenuMixin',
153 'StructuralSubscriptionTargetTraversalMixin',
154 'StructuralSubscriptionView',
155@@ -33,7 +33,10 @@
156 from zope.traversing.browser import absoluteURL
157
158 from canonical.launchpad.webapp.authorization import check_permission
159-from canonical.launchpad.webapp.menu import Link
160+from canonical.launchpad.webapp.menu import (
161+ enabled_with_permission,
162+ Link,
163+ )
164 from canonical.launchpad.webapp.publisher import (
165 canonical_url,
166 LaunchpadView,
167@@ -329,6 +332,14 @@
168 class StructuralSubscriptionMenuMixin:
169 """Mix-in class providing the subscription add/edit menu link."""
170
171+ def _getSST(self):
172+ if IStructuralSubscriptionTarget.providedBy(self.context):
173+ sst = self.context
174+ else:
175+ # self.context is a view, and the target is its context
176+ sst = self.context.context
177+ return sst
178+
179 def subscribe(self):
180 """The subscribe menu link.
181
182@@ -337,28 +348,41 @@
183 and displays the edit icon. Otherwise, the link offers to subscribe
184 and displays the add icon.
185 """
186- if IStructuralSubscriptionTarget.providedBy(self.context):
187- sst = self.context
188- else:
189- # self.context is a view, and the target is its context
190- sst = self.context.context
191+ sst = self._getSST()
192
193 # ProjectGroup milestones aren't really structural subscription
194 # targets as they're not real milestones, so you can't subscribe to
195 # them.
196 enabled = not IProjectGroupMilestone.providedBy(sst)
197-
198 if sst.userHasBugSubscriptions(self.user):
199 text = 'Edit bug mail subscription'
200 icon = 'edit'
201 else:
202 text = 'Subscribe to bug mail'
203 icon = 'add'
204- if enabled == False or (
205+ if not enabled or (
206 not sst.userCanAlterBugSubscription(self.user, self.user)):
207- return Link('+subscribe', text, icon=icon, enabled=False)
208- else:
209- return Link('+subscribe', text, icon=icon, enabled=enabled)
210+ enabled = False
211+ return Link('+subscribe', text, icon=icon, enabled=enabled)
212+
213+ @enabled_with_permission('launchpad.AnyPerson')
214+ def subscribe_to_bug_mail(self):
215+ sst = self._getSST()
216+ enabled = sst.userCanAlterBugSubscription(self.user, self.user)
217+ text = 'Subscribe to bug mail'
218+ return Link('#', text, icon='add', hidden=True, enabled=enabled)
219+
220+
221+def expose_structural_subscription_data_to_js(context, request,
222+ user, subscriptions=None):
223+ """Expose all of the data for a structural subscription to JavaScript."""
224+ expose_user_administered_teams_to_js(request, user)
225+ expose_enum_to_js(request, BugTaskImportance, 'importances')
226+ expose_enum_to_js(request, BugTaskStatus, 'statuses')
227+ if subscriptions is None:
228+ subscriptions = []
229+ expose_user_subscriptions_to_js(
230+ user, subscriptions, request)
231
232
233 def expose_enum_to_js(request, enum, name):
234@@ -424,25 +448,6 @@
235 IJSONRequestCache(request).objects['subscription_info'] = info
236
237
238-class StructuralSubscriptionJSMixin:
239- """A mixin that exposes structural-subscription data in JS.
240-
241- Descendants of this mixin must define a `subscriptions` property
242- that returns a list of the subscriptions to cache in the JS of the
243- page.
244- """
245-
246- def initialize(self):
247- super(StructuralSubscriptionJSMixin, self).initialize()
248- expose_user_administered_teams_to_js(self.request, self.user)
249- expose_user_subscriptions_to_js(
250- self.user, self.subscriptions, self.request)
251- expose_enum_to_js(self.request, BugTaskImportance, 'importances')
252- expose_enum_to_js(self.request, BugTaskStatus, 'statuses')
253-
254- subscriptions = None # Override this.
255-
256-
257 class StructuralSubscribersPortletView(LaunchpadView):
258 """A simple view for displaying the subscribers portlet."""
259
260
261=== modified file 'lib/lp/bugs/templates/bug-subscription-list.pt'
262--- lib/lp/bugs/templates/bug-subscription-list.pt 2011-03-25 21:00:51 +0000
263+++ lib/lp/bugs/templates/bug-subscription-list.pt 2011-03-29 22:36:23 +0000
264@@ -12,9 +12,11 @@
265
266 <head>
267 <tal:head-epilogue metal:fill-slot="head_epilogue">
268- <script type="text/javascript">
269+ <script type="text/javascript"
270+ tal:condition="
271+ request/features/malone.advanced-structural-subscriptions.enabled">
272 LPS.use('lp.registry.structural_subscription', function(Y) {
273- module = Y.lp.registry.structural_subscription;
274+ var module = Y.lp.registry.structural_subscription;
275 Y.on('domready', function() {
276 module.setup_bug_subscriptions(
277 {content_box: "#structural-subscription-content-box"})
278
279=== modified file 'lib/lp/bugs/templates/buglisting-default.pt'
280--- lib/lp/bugs/templates/buglisting-default.pt 2011-02-24 14:53:05 +0000
281+++ lib/lp/bugs/templates/buglisting-default.pt 2011-03-29 22:36:23 +0000
282@@ -10,6 +10,16 @@
283 <metal:block fill-slot="head_epilogue">
284 <meta condition="not: view/should_show_bug_information"
285 name="robots" content="noindex,nofollow" />
286+ <script type="text/javascript"
287+ tal:condition="
288+ request/features/malone.advanced-structural-subscriptions.enabled">
289+ LPS.use('lp.registry.structural_subscription', function(Y) {
290+ var module = Y.lp.registry.structural_subscription;
291+ Y.on('domready', function() {
292+ module.setup({content_box: "#structural-subscription-content-box"});
293+ });
294+ });
295+ </script>
296 </metal:block>
297
298 <body>
299@@ -61,6 +71,9 @@
300 use-macro="context/@@+bugtask-macros-tableview/advanced_search_form" />
301 </tal:show_advanced_form>
302
303+ <div class="yui-u">
304+ <div id="structural-subscription-content-box"></div>
305+ </div>
306
307 </div>
308 <div tal:condition="view/bug_tracking_usage/enumvalue:UNKNOWN"
309
310=== modified file 'lib/lp/bugs/templates/bugtarget-bugs.pt'
311--- lib/lp/bugs/templates/bugtarget-bugs.pt 2010-12-20 16:01:50 +0000
312+++ lib/lp/bugs/templates/bugtarget-bugs.pt 2011-03-29 22:36:23 +0000
313@@ -12,11 +12,22 @@
314 <metal:block fill-slot="head_epilogue">
315 <meta tal:condition="not: view/bug_tracking_usage/enumvalue:LAUNCHPAD"
316 name="robots" content="noindex,nofollow" />
317+ <script type="text/javascript"
318+ tal:condition="
319+ request/features/malone.advanced-structural-subscriptions.enabled">
320+ LPS.use('lp.registry.structural_subscription', function(Y) {
321+ var module = Y.lp.registry.structural_subscription;
322+ Y.on('domready', function() {
323+ module.setup({content_box: "#structural-subscription-content-box"});
324+ });
325+ });
326+ </script>
327 <style type="text/css">
328 p#more-hot-bugs {float:right; margin-top:7px;}
329 </style>
330- </metal:block>
331+</metal:block>
332 <body>
333+
334 <tal:side metal:fill-slot="side"
335 condition="view/bug_tracking_usage/enumvalue:LAUNCHPAD">
336 <div id="involvement" class="portlet">
337@@ -155,7 +166,7 @@
338 </table>
339 <p id="more-hot-bugs"
340 tal:condition="view/hot_bugs_info/has_more_bugs">
341- <a tal:attributes="href string:${context/fmt:url/+bugs}?orderby=-heat&field.status%3Alist=NEW&field.status%3Alist=INCOMPLETE_WITH_RESPONSE&field.status%3Alist=INCOMPLETE_WITHOUT_RESPONSE&field.status%3Alist=CONFIRMED&field.status%3Alist=TRIAGED&field.status%3Alist=INPROGRESS&field.status%3Alist=FIXCOMMITTED&field.omit_dupes=on">Show all bugs by heat</a>
342+ <a tal:attributes="href string:${context/fmt:url/+bugs}?orderby=-heat&amp;field.status%3Alist=NEW&amp;field.status%3Alist=INCOMPLETE_WITH_RESPONSE&amp;field.status%3Alist=INCOMPLETE_WITHOUT_RESPONSE&amp;field.status%3Alist=CONFIRMED&amp;field.status%3Alist=TRIAGED&amp;field.status%3Alist=INPROGRESS&amp;field.status%3Alist=FIXCOMMITTED&amp;field.omit_dupes=on">Show all bugs by heat</a>
343 </p>
344 </div>
345
346@@ -213,6 +224,10 @@
347 </p>
348 </div>
349
350+ <div class="yui-u">
351+ <div id="structural-subscription-content-box"></div>
352+ </div>
353+
354 </div><!-- main -->
355 </body>
356 </html>
357
358=== modified file 'lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt'
359--- lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt 2010-08-16 23:28:48 +0000
360+++ lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt 2011-03-29 22:36:23 +0000
361@@ -10,21 +10,46 @@
362 <tbody id="bugfilters-portlet-content"
363 tal:content="structure context/@@+bugtarget-portlet-bugfilters-info" />
364 <tbody tal:define="menu context/menu:bugs">
365- <tr tal:define="subscribe_link menu/subscribe|nothing"
366- tal:condition="python: subscribe_link and subscribe_link.enabled">
367- <td class="bugs-count" style="padding-top: 1em">
368- <a tal:attributes="href subscribe_link/url">
369- <img tal:attributes="src subscribe_link/icon_url" />
370- </a>
371- </td>
372- <td class="bugs-link">
373- <a tal:attributes="href subscribe_link/url"
374- tal:content="subscribe_link/escapedtext" />
375- </td>
376- </tr>
377+
378+ <tal:advanced-structural-subscriptions
379+ condition="request/features/malone.advanced-structural-subscriptions.enabled">
380+ <tr class="menu-link-subscribe_to_bug_mail invisible-link"
381+ tal:define="subscribe_link menu/subscribe_to_bug_mail|nothing"
382+ tal:condition="python: subscribe_link and subscribe_link.enabled">
383+ <td class="bugs-count" style="padding-top: 3px">
384+ <a tal:attributes="href subscribe_link/url">
385+ <img tal:attributes="src subscribe_link/icon_url" />
386+ </a>
387+ </td>
388+ <td class="bugs-link">
389+ <a class="js-action"
390+ tal:attributes="href subscribe_link/url"
391+ tal:content="subscribe_link/escapedtext" />
392+ </td>
393+ </tr>
394+ </tal:advanced-structural-subscriptions>
395+
396+ <tal:not-advanced-structural-subscriptions
397+ condition="not: request/features/malone.advanced-structural-subscriptions.enabled">
398+ <tr class="menu-link-subscribe"
399+ tal:define="subscribe_link menu/subscribe|nothing"
400+ tal:condition="python: subscribe_link and subscribe_link.enabled">
401+ <td class="bugs-count" style="padding-top: 3px">
402+ <a tal:attributes="href subscribe_link/url">
403+ <img tal:attributes="src subscribe_link/icon_url" />
404+ </a>
405+ </td>
406+ <td class="bugs-link">
407+ <a tal:attributes="href subscribe_link/url"
408+ tal:content="subscribe_link/escapedtext" />
409+ </td>
410+ </tr>
411+ </tal:not-advanced-structural-subscriptions>
412+
413 <tr tal:define="review_nominations_link context/menu:bugs/nominations|nothing"
414- tal:condition="review_nominations_link">
415- <td class="bugs-count" style="padding-top: 1em">
416+ tal:condition="review_nominations_link"
417+ style="padding-top: 1em">
418+ <td class="bugs-count">
419 <a tal:attributes="href review_nominations_link/url">
420 <img tal:attributes="src review_nominations_link/icon_url" />
421 </a>
422
423=== modified file 'lib/lp/bugs/templates/bugtarget-subscription-list.pt'
424--- lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-03-25 21:00:51 +0000
425+++ lib/lp/bugs/templates/bugtarget-subscription-list.pt 2011-03-29 22:36:23 +0000
426@@ -12,9 +12,11 @@
427
428 <head>
429 <tal:head-epilogue metal:fill-slot="head_epilogue">
430- <script type="text/javascript">
431+ <script type="text/javascript"
432+ tal:condition="
433+ request/features/malone.advanced-structural-subscriptions.enabled">
434 LPS.use('lp.registry.structural_subscription', function(Y) {
435- module = Y.lp.registry.structural_subscription;
436+ var module = Y.lp.registry.structural_subscription;
437 Y.on('domready', function() {
438 module.setup_bug_subscriptions(
439 {content_box: "#structural-subscription-content-box"})
440
441=== modified file 'lib/lp/registry/browser/__init__.py'
442--- lib/lp/registry/browser/__init__.py 2011-02-21 15:22:33 +0000
443+++ lib/lp/registry/browser/__init__.py 2011-03-29 22:36:23 +0000
444@@ -6,6 +6,7 @@
445 __metaclass__ = type
446
447 __all__ = [
448+ 'add_subscribe_link',
449 'BaseRdfView',
450 'get_status_counts',
451 'MilestoneOverlayMixin',
452@@ -38,6 +39,7 @@
453 )
454 from lp.registry.interfaces.productseries import IProductSeries
455 from lp.registry.interfaces.series import SeriesStatus
456+from lp.services.features import getFeatureFlag
457
458
459 class StatusCount:
460@@ -69,6 +71,16 @@
461 for status in sorted(statuses, key=attrgetter(key))]
462
463
464+def add_subscribe_link(links):
465+ """Based on a feature flag, add the correct link."""
466+ use_advanced_features = getFeatureFlag(
467+ 'malone.advanced-structural-subscriptions.enabled')
468+ if use_advanced_features:
469+ links.append('subscribe_to_bug_mail')
470+ else:
471+ links.append('subscribe')
472+
473+
474 class MilestoneOverlayMixin:
475 """A mixin that provides the data for the milestoneoverlay script."""
476
477
478=== modified file 'lib/lp/registry/browser/distribution.py'
479--- lib/lp/registry/browser/distribution.py 2011-03-04 09:55:17 +0000
480+++ lib/lp/registry/browser/distribution.py 2011-03-29 22:36:23 +0000
481@@ -83,9 +83,14 @@
482 )
483 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
484 from lp.bugs.browser.structuralsubscription import (
485+ expose_structural_subscription_data_to_js,
486+ StructuralSubscriptionMenuMixin,
487 StructuralSubscriptionTargetTraversalMixin,
488 )
489-from lp.registry.browser import RegistryEditFormView
490+from lp.registry.browser import (
491+ add_subscribe_link,
492+ RegistryEditFormView,
493+ )
494 from lp.registry.browser.announcement import HasAnnouncementsView
495 from lp.registry.browser.menu import (
496 IRegistryCollectionNavigationMenu,
497@@ -104,6 +109,7 @@
498 MirrorSpeed,
499 )
500 from lp.registry.interfaces.series import SeriesStatus
501+from lp.services.features import getFeatureFlag
502 from lp.services.geoip.helpers import (
503 ipaddress_from_request,
504 request_country,
505@@ -265,8 +271,8 @@
506 return Link('+unofficialmirrors', text, enabled=enabled, icon='info')
507
508
509-class DistributionLinksMixin:
510- """A mixing to provide common links to menus."""
511+class DistributionLinksMixin(StructuralSubscriptionMenuMixin):
512+ """A mixin to provide common links to menus."""
513
514 @enabled_with_permission('launchpad.Edit')
515 def edit(self):
516@@ -278,7 +284,15 @@
517 """A menu of context actions."""
518 usedfor = IDistribution
519 facet = 'overview'
520- links = ['edit']
521+
522+ @cachedproperty
523+ def links(self):
524+ links = ['edit']
525+ use_advanced_features = getFeatureFlag(
526+ 'malone.advanced-structural-subscriptions.enabled')
527+ if use_advanced_features:
528+ links.append('subscribe_to_bug_mail')
529+ return links
530
531
532 class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin):
533@@ -448,13 +462,17 @@
534
535 usedfor = IDistribution
536 facet = 'bugs'
537- links = (
538- 'bugsupervisor',
539- 'securitycontact',
540- 'cve',
541- 'filebug',
542- 'subscribe',
543- )
544+
545+ @property
546+ def links(self):
547+ links = [
548+ 'bugsupervisor',
549+ 'securitycontact',
550+ 'cve',
551+ 'filebug',
552+ ]
553+ add_subscribe_link(links)
554+ return links
555
556
557 class DistributionSpecificationsMenu(NavigationMenu,
558@@ -594,6 +612,11 @@
559 class DistributionView(HasAnnouncementsView, FeedsMixin):
560 """Default Distribution view class."""
561
562+ def initialize(self):
563+ super(DistributionView, self).initialize()
564+ expose_structural_subscription_data_to_js(
565+ self.context, self.request, self.user)
566+
567 def linkedMilestonesForSeries(self, series):
568 """Return a string of linkified milestones in the series."""
569 # Listify to remove repeated queries.
570
571=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
572--- lib/lp/registry/browser/distributionsourcepackage.py 2011-03-23 16:28:51 +0000
573+++ lib/lp/registry/browser/distributionsourcepackage.py 2011-03-29 22:36:23 +0000
574@@ -61,9 +61,12 @@
575 from lp.app.interfaces.launchpad import IServiceUsage
576 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
577 from lp.bugs.browser.structuralsubscription import (
578+ expose_structural_subscription_data_to_js,
579+ StructuralSubscriptionMenuMixin,
580 StructuralSubscriptionTargetTraversalMixin,
581 )
582 from lp.bugs.interfaces.bug import IBugSet
583+from lp.registry.browser import add_subscribe_link
584 from lp.registry.browser.pillar import PillarBugsMenu
585 from lp.registry.interfaces.distributionsourcepackage import (
586 IDistributionSourcePackage,
587@@ -123,9 +126,6 @@
588
589 class DistributionSourcePackageLinksMixin:
590
591- def subscribe(self):
592- return Link('+subscribe', 'Subscribe to bug mail', icon='edit')
593-
594 def publishinghistory(self):
595 return Link('+publishinghistory', 'Show publishing history')
596
597@@ -152,17 +152,22 @@
598
599 usedfor = IDistributionSourcePackage
600 facet = 'overview'
601- links = [
602- 'subscribe', 'publishinghistory', 'edit', 'new_bugs',
603- 'open_questions']
604+ links = ['new_bugs', 'open_questions']
605
606
607 class DistributionSourcePackageBugsMenu(
608- PillarBugsMenu, DistributionSourcePackageLinksMixin):
609+ PillarBugsMenu,
610+ StructuralSubscriptionMenuMixin,
611+ DistributionSourcePackageLinksMixin):
612
613 usedfor = IDistributionSourcePackage
614 facet = 'bugs'
615- links = ['filebug', 'subscribe']
616+
617+ @cachedproperty
618+ def links(self):
619+ links = ['filebug']
620+ add_subscribe_link(links)
621+ return links
622
623
624 class DistributionSourcePackageNavigation(Navigation,
625@@ -217,12 +222,20 @@
626
627
628 class DistributionSourcePackageActionMenu(
629- NavigationMenu, DistributionSourcePackageLinksMixin):
630+ NavigationMenu,
631+ StructuralSubscriptionMenuMixin,
632+ DistributionSourcePackageLinksMixin):
633 """Action menu for distro source packages."""
634 usedfor = IDistributionSourcePackageActionMenu
635 facet = 'overview'
636 title = 'Actions'
637- links = ('publishing_history', 'change_log', 'subscribe', 'edit')
638+
639+ @cachedproperty
640+ def links(self):
641+ links = ['publishing_history', 'change_log']
642+ add_subscribe_link(links)
643+ links.append('edit')
644+ return links
645
646 def publishing_history(self):
647 text = 'View full publishing history'
648@@ -295,6 +308,11 @@
649 """View class for DistributionSourcePackage."""
650 implements(IDistributionSourcePackageActionMenu)
651
652+ def initialize(self):
653+ super(DistributionSourcePackageView, self).initialize()
654+ expose_structural_subscription_data_to_js(
655+ self.context, self.request, self.user)
656+
657 @property
658 def label(self):
659 return self.context.title
660
661=== modified file 'lib/lp/registry/browser/distroseries.py'
662--- lib/lp/registry/browser/distroseries.py 2011-03-29 18:09:43 +0000
663+++ lib/lp/registry/browser/distroseries.py 2011-03-29 22:36:23 +0000
664@@ -75,10 +75,14 @@
665 )
666 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
667 from lp.bugs.browser.structuralsubscription import (
668+ expose_structural_subscription_data_to_js,
669 StructuralSubscriptionMenuMixin,
670 StructuralSubscriptionTargetTraversalMixin,
671 )
672-from lp.registry.browser import MilestoneOverlayMixin
673+from lp.registry.browser import (
674+ add_subscribe_link,
675+ MilestoneOverlayMixin,
676+ )
677 from lp.registry.enum import DistroSeriesDifferenceStatus
678 from lp.registry.interfaces.distroseries import IDistroSeries
679 from lp.registry.interfaces.distroseriesdifference import (
680@@ -186,9 +190,23 @@
681
682 usedfor = IDistroSeries
683 facet = 'overview'
684- links = ['edit', 'reassign', 'driver', 'answers',
685- 'packaging', 'needs_packaging', 'builds', 'queue',
686- 'add_port', 'create_milestone', 'subscribe', 'admin']
687+
688+ @property
689+ def links(self):
690+ links = ['edit',
691+ 'reassign',
692+ 'driver',
693+ 'answers',
694+ 'packaging',
695+ 'needs_packaging',
696+ 'builds',
697+ 'queue',
698+ 'add_port',
699+ 'create_milestone',
700+ ]
701+ add_subscribe_link(links)
702+ links.append('admin')
703+ return links
704
705 @enabled_with_permission('launchpad.Admin')
706 def edit(self):
707@@ -252,11 +270,14 @@
708
709 usedfor = IDistroSeries
710 facet = 'bugs'
711- links = (
712- 'cve',
713- 'nominations',
714- 'subscribe',
715- )
716+
717+ @property
718+ def links(self):
719+ links = ['cve',
720+ 'nominations',
721+ ]
722+ add_subscribe_link(links)
723+ return links
724
725 def cve(self):
726 return Link('+cve', 'CVE reports', icon='cve')
727@@ -327,12 +348,15 @@
728 self.context.datereleased = UTC_NOW
729
730
731-class DistroSeriesView(MilestoneOverlayMixin):
732+class DistroSeriesView(LaunchpadView, MilestoneOverlayMixin):
733
734 def initialize(self):
735+ super(DistroSeriesView, self).initialize()
736 self.displayname = '%s %s' % (
737 self.context.distribution.displayname,
738 self.context.version)
739+ expose_structural_subscription_data_to_js(
740+ self.context, self.request, self.user)
741
742 @property
743 def page_title(self):
744
745=== modified file 'lib/lp/registry/browser/milestone.py'
746--- lib/lp/registry/browser/milestone.py 2011-03-04 00:55:49 +0000
747+++ lib/lp/registry/browser/milestone.py 2011-03-29 22:36:23 +0000
748@@ -64,6 +64,7 @@
749 get_status_counts,
750 RegistryDeleteViewMixin,
751 )
752+from lp.registry.browser import add_subscribe_link
753 from lp.registry.browser.product import ProductDownloadFileMixin
754 from lp.registry.interfaces.distroseries import IDistroSeries
755 from lp.registry.interfaces.milestone import (
756@@ -140,14 +141,25 @@
757 class MilestoneContextMenu(ContextMenu, MilestoneLinkMixin):
758 """The menu for this milestone."""
759 usedfor = IMilestone
760- links = ['edit', 'subscribe', 'create_release']
761+
762+ @cachedproperty
763+ def links(self):
764+ links = ['edit']
765+ add_subscribe_link(links)
766+ links.append('create_release')
767+ return links
768
769
770 class MilestoneOverviewNavigationMenu(NavigationMenu, MilestoneLinkMixin):
771 """Overview navigation menu for `IMilestone` objects."""
772 usedfor = IMilestone
773 facet = 'overview'
774- links = ('edit', 'delete', 'subscribe')
775+
776+ @cachedproperty
777+ def links(self):
778+ links = ['edit', 'delete']
779+ add_subscribe_link(links)
780+ return links
781
782
783 class MilestoneOverviewMenu(ApplicationMenu, MilestoneLinkMixin):
784
785=== modified file 'lib/lp/registry/browser/product.py'
786--- lib/lp/registry/browser/product.py 2011-03-23 15:55:44 +0000
787+++ lib/lp/registry/browser/product.py 2011-03-29 22:36:23 +0000
788@@ -153,14 +153,13 @@
789 BugTargetTraversalMixin,
790 get_buglisting_search_filter_url,
791 )
792-from lp.bugs.interfaces.bugtask import (
793- RESOLVED_BUGTASK_STATUSES,
794- BugTaskImportance,
795- BugTaskStatus,
796- )
797+from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
798 from lp.code.browser.branchref import BranchRef
799 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
800-from lp.registry.browser import BaseRdfView
801+from lp.registry.browser import (
802+ add_subscribe_link,
803+ BaseRdfView,
804+ )
805 from lp.registry.browser.announcement import HasAnnouncementsView
806 from lp.registry.browser.branding import BrandingChangeView
807 from lp.registry.browser.menu import (
808@@ -173,8 +172,7 @@
809 )
810 from lp.registry.browser.productseries import get_series_branch_error
811 from lp.bugs.browser.structuralsubscription import (
812- expose_enum_to_js,
813- expose_user_administered_teams_to_js,
814+ expose_structural_subscription_data_to_js,
815 StructuralSubscriptionMenuMixin,
816 StructuralSubscriptionTargetTraversalMixin,
817 )
818@@ -193,7 +191,6 @@
819 from lp.registry.interfaces.productseries import IProductSeries
820 from lp.registry.interfaces.series import SeriesStatus
821 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
822-from lp.services import features
823 from lp.services.fields import (
824 PillarAliases,
825 PublicPersonChoice,
826@@ -585,22 +582,12 @@
827 facet = 'overview'
828 title = 'Actions'
829
830- @property
831+ @cachedproperty
832 def links(self):
833 links = ['edit', 'review_license', 'administer']
834- use_advanced_features = features.getFeatureFlag(
835- 'advanced-structural-subscriptions.enabled')
836- if use_advanced_features:
837- links.append('subscribe_to_bug_mail')
838- else:
839- links.append('subscribe')
840+ add_subscribe_link(links)
841 return links
842
843- @enabled_with_permission('launchpad.AnyPerson')
844- def subscribe_to_bug_mail(self):
845- text = 'Subscribe to bug mail'
846- return Link('#', text, icon='add', hidden=True)
847-
848
849 class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin,
850 HasRecipesMenuMixin):
851@@ -694,16 +681,20 @@
852
853 usedfor = IProduct
854 facet = 'bugs'
855- links = (
856- 'filebug',
857- 'bugsupervisor',
858- 'securitycontact',
859- 'cve',
860- 'subscribe',
861- 'configure_bugtracker',
862- )
863 configurable_bugtracker = True
864
865+ @cachedproperty
866+ def links(self):
867+ links = [
868+ 'filebug',
869+ 'bugsupervisor',
870+ 'securitycontact',
871+ 'cve',
872+ ]
873+ add_subscribe_link(links)
874+ links.append('configure_bugtracker')
875+ return links
876+
877
878 class ProductSpecificationsMenu(NavigationMenu, ProductEditLinksMixin,
879 HasSpecificationsMenuMixin):
880@@ -1009,6 +1000,7 @@
881 self.form = request.form_ng
882
883 def initialize(self):
884+ super(ProductView, self).initialize()
885 self.status_message = None
886 product = self.context
887 title_field = IProduct['title']
888@@ -1028,9 +1020,8 @@
889 self.show_programming_languages = bool(
890 self.context.programminglang or
891 check_permission('launchpad.Edit', self.context))
892- expose_user_administered_teams_to_js(self.request, self.user)
893- expose_enum_to_js(self.request, BugTaskImportance, 'importances')
894- expose_enum_to_js(self.request, BugTaskStatus, 'statuses')
895+ expose_structural_subscription_data_to_js(
896+ self.context, self.request, self.user)
897
898 @property
899 def show_license_status(self):
900
901=== modified file 'lib/lp/registry/browser/productseries.py'
902--- lib/lp/registry/browser/productseries.py 2011-02-03 10:35:36 +0000
903+++ lib/lp/registry/browser/productseries.py 2011-03-29 22:36:23 +0000
904@@ -118,6 +118,7 @@
905 ICodeImportSet,
906 )
907 from lp.registry.browser import (
908+ add_subscribe_link,
909 BaseRdfView,
910 MilestoneOverlayMixin,
911 RegistryDeleteViewMixin,
912@@ -128,6 +129,7 @@
913 PillarView,
914 )
915 from lp.bugs.browser.structuralsubscription import (
916+ expose_structural_subscription_data_to_js,
917 StructuralSubscriptionMenuMixin,
918 StructuralSubscriptionTargetTraversalMixin,
919 )
920@@ -279,19 +281,23 @@
921 """The overview menu."""
922 usedfor = IProductSeries
923 facet = 'overview'
924- links = [
925- 'configure_bugtracker',
926- 'create_milestone',
927- 'create_release',
928- 'delete',
929- 'driver',
930- 'edit',
931- 'link_branch',
932- 'rdf',
933- 'set_branch',
934- 'subscribe',
935- 'ubuntupkg',
936- ]
937+
938+ @cachedproperty
939+ def links(self):
940+ links = [
941+ 'configure_bugtracker',
942+ 'create_milestone',
943+ 'create_release',
944+ 'delete',
945+ 'driver',
946+ 'edit',
947+ 'link_branch',
948+ 'rdf',
949+ 'set_branch',
950+ ]
951+ add_subscribe_link(links)
952+ links.append('ubuntupkg')
953+ return links
954
955 @enabled_with_permission('launchpad.Edit')
956 def configure_bugtracker(self):
957@@ -380,11 +386,12 @@
958 """The bugs menu."""
959 usedfor = IProductSeries
960 facet = 'bugs'
961- links = (
962- 'new',
963- 'nominations',
964- 'subscribe',
965- )
966+
967+ @cachedproperty
968+ def links(self):
969+ links = ['new', 'nominations']
970+ add_subscribe_link(links)
971+ return links
972
973 def new(self):
974 """Return a link to report a bug in this series."""
975@@ -439,6 +446,11 @@
976 class ProductSeriesView(LaunchpadView, MilestoneOverlayMixin):
977 """A view to show a series with translations."""
978
979+ def initialize(self):
980+ super(ProductSeriesView, self).initialize()
981+ expose_structural_subscription_data_to_js(
982+ self.context, self.request, self.user)
983+
984 @property
985 def page_title(self):
986 """Return the HTML page title."""
987
988=== modified file 'lib/lp/registry/browser/project.py'
989--- lib/lp/registry/browser/project.py 2011-01-21 08:30:55 +0000
990+++ lib/lp/registry/browser/project.py 2011-03-29 22:36:23 +0000
991@@ -74,7 +74,10 @@
992 from lp.blueprints.browser.specificationtarget import (
993 HasSpecificationsMenuMixin,
994 )
995-from lp.registry.browser import BaseRdfView
996+from lp.registry.browser import (
997+ add_subscribe_link,
998+ BaseRdfView,
999+ )
1000 from lp.registry.browser.announcement import HasAnnouncementsView
1001 from lp.registry.browser.branding import BrandingChangeView
1002 from lp.registry.browser.menu import (
1003@@ -88,6 +91,8 @@
1004 ProjectAddStepTwo,
1005 )
1006 from lp.bugs.browser.structuralsubscription import (
1007+ expose_structural_subscription_data_to_js,
1008+ StructuralSubscriptionMenuMixin,
1009 StructuralSubscriptionTargetTraversalMixin,
1010 )
1011 from lp.registry.interfaces.product import IProductSet
1012@@ -270,20 +275,20 @@
1013 """Marker interface for views that use ProjectActionMenu."""
1014
1015
1016-class ProjectActionMenu(ProjectAdminMenuMixin, NavigationMenu):
1017+class ProjectActionMenu(ProjectAdminMenuMixin,
1018+ StructuralSubscriptionMenuMixin,
1019+ NavigationMenu):
1020
1021 usedfor = IProjectGroupActionMenu
1022 facet = 'overview'
1023 title = 'Action menu'
1024- links = ('subscribe', 'edit', 'administer')
1025
1026- # XXX: salgado, bug=412178, 2009-08-10: This should be shown in the +index
1027- # page of the project's bugs facet, but that would require too much work
1028- # and I just want to convert this page to 3.0, so I'll leave it here for
1029- # now.
1030- def subscribe(self):
1031- text = 'Subscribe to bug mail'
1032- return Link('+subscribe', text, icon='edit')
1033+ @cachedproperty
1034+ def links(self):
1035+ links = []
1036+ add_subscribe_link(links)
1037+ links.extend(['edit', 'administer'])
1038+ return links
1039
1040 @enabled_with_permission('launchpad.Edit')
1041 def edit(self):
1042@@ -323,24 +328,31 @@
1043 return Link('+addquestion', text, icon='add')
1044
1045
1046-class ProjectBugsMenu(ApplicationMenu):
1047+class ProjectBugsMenu(StructuralSubscriptionMenuMixin,
1048+ ApplicationMenu):
1049
1050 usedfor = IProjectGroup
1051 facet = 'bugs'
1052- links = ['new', 'subscribe']
1053+
1054+ @cachedproperty
1055+ def links(self):
1056+ links = ['new']
1057+ add_subscribe_link(links)
1058+ return links
1059
1060 def new(self):
1061 text = 'Report a Bug'
1062 return Link('+filebug', text, icon='add')
1063
1064- def subscribe(self):
1065- text = 'Subscribe to bug mail'
1066- return Link('+subscribe', text, icon='edit')
1067-
1068
1069 class ProjectView(HasAnnouncementsView, FeedsMixin):
1070 implements(IProjectGroupActionMenu)
1071
1072+ def initialize(self):
1073+ super(ProjectView, self).initialize()
1074+ expose_structural_subscription_data_to_js(
1075+ self.context, self.request, self.user)
1076+
1077 @cachedproperty
1078 def has_many_projects(self):
1079 """Does the projectgroup have many sub projects.
1080
1081=== modified file 'lib/lp/registry/browser/tests/test_product.py'
1082--- lib/lp/registry/browser/tests/test_product.py 2011-03-07 19:53:40 +0000
1083+++ lib/lp/registry/browser/tests/test_product.py 2011-03-29 22:36:23 +0000
1084@@ -13,10 +13,14 @@
1085 from zope.security.proxy import removeSecurityProxy
1086
1087 from canonical.config import config
1088-from canonical.launchpad.testing.pages import find_tag_by_id
1089+from canonical.launchpad.testing.pages import (
1090+ find_tag_by_id,
1091+ )
1092 from canonical.testing.layers import DatabaseFunctionalLayer
1093 from lp.app.enums import ServiceUsage
1094-from lp.registry.browser.product import ProductLicenseMixin
1095+from lp.registry.browser.product import (
1096+ ProductLicenseMixin,
1097+ )
1098 from lp.registry.interfaces.product import (
1099 License,
1100 IProductSet,
1101
1102=== added file 'lib/lp/registry/browser/tests/test_subscription_links.py'
1103--- lib/lp/registry/browser/tests/test_subscription_links.py 1970-01-01 00:00:00 +0000
1104+++ lib/lp/registry/browser/tests/test_subscription_links.py 2011-03-29 22:36:23 +0000
1105@@ -0,0 +1,609 @@
1106+# Copyright 2011 Canonical Ltd. This software is licensed under the
1107+# GNU Affero General Public License version 3 (see the file LICENSE).
1108+
1109+"""Tests for subscription links."""
1110+
1111+__metaclass__ = type
1112+
1113+import unittest
1114+from zope.component import getUtility
1115+from BeautifulSoup import BeautifulSoup
1116+
1117+from canonical.launchpad.webapp.interaction import ANONYMOUS
1118+from canonical.launchpad.webapp.interfaces import ILaunchBag
1119+from canonical.launchpad.webapp.publisher import canonical_url
1120+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
1121+from canonical.launchpad.testing.pages import (
1122+ first_tag_by_class,
1123+ )
1124+from canonical.testing.layers import DatabaseFunctionalLayer
1125+
1126+from lp.registry.interfaces.person import IPersonSet
1127+from lp.services.features import (
1128+ get_relevant_feature_controller,
1129+ )
1130+from lp.services.features.testing import FeatureFixture
1131+from lp.testing import (
1132+ celebrity_logged_in,
1133+ person_logged_in,
1134+ BrowserTestCase,
1135+ TestCaseWithFactory,
1136+ )
1137+from lp.testing.sampledata import ADMIN_EMAIL
1138+from lp.testing.views import (
1139+ create_initialized_view,
1140+ )
1141+
1142+
1143+class _TestStructSubs(TestCaseWithFactory):
1144+ """Test structural subscriptions base class.
1145+
1146+ The link to structural subscriptions is controlled by the feature flag
1147+ 'malone.advanced-structural-subscriptions.enabled'. If it is false, the
1148+ old link leading to +subscribe is shown. If it is true then the new
1149+ JavaScript control is used.
1150+ """
1151+
1152+ layer = DatabaseFunctionalLayer
1153+ feature_flag = 'malone.advanced-structural-subscriptions.enabled'
1154+
1155+ def setUp(self):
1156+ super(_TestStructSubs, self).setUp()
1157+ self.regular_user = self.factory.makePerson()
1158+
1159+ def _create_scenario(self, user, flag):
1160+ with person_logged_in(user):
1161+ with FeatureFixture({self.feature_flag: flag}):
1162+ view = self.create_view(user)
1163+ self.contents = view.render()
1164+ old_link = first_tag_by_class(
1165+ self.contents, 'menu-link-subscribe')
1166+ new_link = first_tag_by_class(
1167+ self.contents, 'menu-link-subscribe_to_bug_mail')
1168+ return old_link, new_link
1169+
1170+ def create_view(self, user):
1171+ request = LaunchpadTestRequest(
1172+ PATH_INFO='/', HTTP_COOKIE='', QUERY_STRING='')
1173+ request.features = get_relevant_feature_controller()
1174+ return create_initialized_view(
1175+ self.target, self.view, principal=user,
1176+ rootsite=self.rootsite,
1177+ request=request, current_request=False)
1178+
1179+ def test_subscribe_link_feature_flag_off_owner(self):
1180+ old_link, new_link = self._create_scenario(
1181+ self.target.owner, None)
1182+ self.assertNotEqual(None, old_link, self.contents)
1183+ self.assertEqual(None, new_link, self.contents)
1184+
1185+ def test_subscribe_link_feature_flag_on_owner(self):
1186+ # Test the new subscription link.
1187+ old_link, new_link = self._create_scenario(
1188+ self.target.owner, 'on')
1189+ self.assertEqual(None, old_link, self.contents)
1190+ self.assertNotEqual(None, new_link, self.contents)
1191+
1192+ def test_subscribe_link_feature_flag_off_user(self):
1193+ old_link, new_link = self._create_scenario(
1194+ self.regular_user, None)
1195+ self.assertNotEqual(None, old_link, self.contents)
1196+ self.assertEqual(None, new_link, self.contents)
1197+
1198+ def test_subscribe_link_feature_flag_on_user(self):
1199+ old_link, new_link = self._create_scenario(
1200+ self.regular_user, 'on')
1201+ self.assertEqual(None, old_link, self.contents)
1202+ self.assertNotEqual(None, new_link, self.contents)
1203+
1204+ def test_subscribe_link_feature_flag_off_anonymous(self):
1205+ old_link, new_link = self._create_scenario(
1206+ ANONYMOUS, None)
1207+ # The old subscribe link is actually shown to anonymous users but the
1208+ # behavior has changed with the new link.
1209+ self.assertNotEqual(None, old_link, self.contents)
1210+ self.assertEqual(None, new_link, self.contents)
1211+
1212+ def test_subscribe_link_feature_flag_on_anonymous(self):
1213+ old_link, new_link = self._create_scenario(
1214+ ANONYMOUS, 'on')
1215+ # The subscribe link is not shown to anonymous.
1216+ self.assertEqual(None, old_link, self.contents)
1217+ self.assertEqual(None, new_link, self.contents)
1218+
1219+
1220+class TestProductViewStructSubs(_TestStructSubs):
1221+ """Test structural subscriptions on the product view."""
1222+
1223+ rootsite = None
1224+ view = '+index'
1225+
1226+ def setUp(self):
1227+ super(TestProductViewStructSubs, self).setUp()
1228+ self.target = self.factory.makeProduct(official_malone=True)
1229+
1230+
1231+class TestProductBugsStructSubs(TestProductViewStructSubs):
1232+ """Test structural subscriptions on the product bugs view."""
1233+
1234+ rootsite = 'bugs'
1235+ view = '+bugs-index'
1236+
1237+
1238+class TestProjectGroupViewStructSubs(_TestStructSubs):
1239+ """Test structural subscriptions on the project group view."""
1240+
1241+ rootsite = None
1242+ view = '+index'
1243+
1244+ def setUp(self):
1245+ super(TestProjectGroupViewStructSubs, self).setUp()
1246+ self.target = self.factory.makeProject()
1247+ self.factory.makeProduct(
1248+ project=self.target, official_malone=True)
1249+
1250+
1251+class TestProjectGroupBugsStructSubs(TestProjectGroupViewStructSubs):
1252+ """Test structural subscriptions on the project group bugs view."""
1253+
1254+ rootsite = 'bugs'
1255+ view = '+bugs'
1256+
1257+
1258+class TestProductSeriesViewStructSubs(_TestStructSubs):
1259+ """Test structural subscriptions on the product series view."""
1260+
1261+ rootsite = None
1262+ view = '+index'
1263+
1264+ def setUp(self):
1265+ super(TestProductSeriesViewStructSubs, self).setUp()
1266+ self.target = self.factory.makeProductSeries()
1267+
1268+
1269+class TestProductSeriesBugsStructSubs(TestProductSeriesViewStructSubs):
1270+ """Test structural subscriptions on the product series bugs view."""
1271+
1272+ rootsite = 'bugs'
1273+ view = '+bugs-index'
1274+
1275+ def setUp(self):
1276+ super(TestProductSeriesBugsStructSubs, self).setUp()
1277+ with person_logged_in(self.target.product.owner):
1278+ self.target.product.official_malone = True
1279+
1280+
1281+class TestDistributionSourcePackageViewStructSubs(_TestStructSubs):
1282+ """Test structural subscriptions on the distro src pkg view."""
1283+
1284+ rootsite = None
1285+ view = '+index'
1286+
1287+ def setUp(self):
1288+ super(TestDistributionSourcePackageViewStructSubs, self).setUp()
1289+ distro = self.factory.makeDistribution()
1290+ with person_logged_in(distro.owner):
1291+ distro.official_malone = True
1292+ self.target = self.factory.makeDistributionSourcePackage(
1293+ distribution=distro)
1294+ self.regular_user = self.factory.makePerson()
1295+
1296+ # DistributionSourcePackages do not have owners.
1297+ test_subscribe_link_feature_flag_off_owner = None
1298+ test_subscribe_link_feature_flag_on_owner = None
1299+
1300+
1301+class TestDistributionSourcePackageBugsStructSubs(
1302+ TestDistributionSourcePackageViewStructSubs):
1303+ """Test structural subscriptions on the distro src pkg bugs view."""
1304+
1305+ rootsite = 'bugs'
1306+ view = '+bugs'
1307+
1308+
1309+class TestDistroViewStructSubs(BrowserTestCase):
1310+ """Test structural subscriptions on the distribution view.
1311+
1312+ Distributions are special. They are IStructuralSubscriptionTargets but
1313+ have complicated rules to ensure Ubuntu users don't subscribe and become
1314+ overwhelmed with email. If a distro does not have a bug supervisor set,
1315+ then anyone can create a structural subscription for themselves. If the
1316+ bug supervisor is set, then only people in the bug supervisor team can
1317+ subscribe themselves. Admins can subscribe anyone.
1318+ """
1319+
1320+ layer = DatabaseFunctionalLayer
1321+ feature_flag = 'malone.advanced-structural-subscriptions.enabled'
1322+ rootsite = None
1323+ view = '+index'
1324+
1325+ def setUp(self):
1326+ super(TestDistroViewStructSubs, self).setUp()
1327+ self.target = self.factory.makeDistribution()
1328+ with person_logged_in(self.target.owner):
1329+ self.target.official_malone = True
1330+ self.regular_user = self.factory.makePerson()
1331+
1332+ def _create_scenario(self, user, flag):
1333+ with person_logged_in(user):
1334+ with FeatureFixture({self.feature_flag: flag}):
1335+ logged_in_user = getUtility(ILaunchBag).user
1336+ no_login = logged_in_user is None
1337+ browser = self.getViewBrowser(
1338+ self.target, view_name=self.view,
1339+ rootsite=self.rootsite,
1340+ no_login=no_login,
1341+ user=logged_in_user)
1342+ self.contents = browser.contents
1343+ soup = BeautifulSoup(self.contents)
1344+ href = canonical_url(
1345+ self.target, rootsite=self.rootsite,
1346+ view_name='+subscribe')
1347+ old_link = soup.find('a', href=href)
1348+ new_link = first_tag_by_class(
1349+ self.contents, 'menu-link-subscribe_to_bug_mail')
1350+ return old_link, new_link
1351+
1352+ def test_subscribe_link_feature_flag_off_owner(self):
1353+ old_link, new_link = self._create_scenario(
1354+ self.target.owner, None)
1355+ self.assertEqual(None, old_link, self.contents)
1356+ self.assertEqual(None, new_link, self.contents)
1357+
1358+ def test_subscribe_link_feature_flag_on_owner(self):
1359+ old_link, new_link = self._create_scenario(
1360+ self.target.owner, 'on')
1361+ self.assertEqual(None, old_link, self.contents)
1362+ self.assertNotEqual(None, new_link, self.contents)
1363+
1364+ def test_subscribe_link_feature_flag_off_user(self):
1365+ old_link, new_link = self._create_scenario(
1366+ self.regular_user, None)
1367+ self.assertEqual(None, old_link, self.contents)
1368+ self.assertEqual(None, new_link, self.contents)
1369+
1370+ def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
1371+ old_link, new_link = self._create_scenario(
1372+ self.regular_user, 'on')
1373+ self.assertEqual(None, old_link, self.contents)
1374+ self.assertNotEqual(None, new_link, self.contents)
1375+
1376+ def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
1377+ with celebrity_logged_in('admin'):
1378+ admin = getUtility(ILaunchBag).user
1379+ supervisor = self.factory.makePerson()
1380+ self.target.setBugSupervisor(
1381+ supervisor, admin)
1382+ old_link, new_link = self._create_scenario(
1383+ self.regular_user, 'on')
1384+ self.assertEqual(None, old_link, self.contents)
1385+ self.assertEqual(None, new_link, self.contents)
1386+
1387+ def test_subscribe_link_feature_flag_off_anonymous(self):
1388+ old_link, new_link = self._create_scenario(
1389+ ANONYMOUS, None)
1390+ self.assertEqual(None, old_link, self.contents)
1391+ self.assertEqual(None, new_link, self.contents)
1392+
1393+ def test_subscribe_link_feature_flag_on_anonymous(self):
1394+ old_link, new_link = self._create_scenario(
1395+ ANONYMOUS, 'on')
1396+ self.assertEqual(None, old_link, self.contents)
1397+ self.assertEqual(None, new_link, self.contents)
1398+
1399+ def test_subscribe_link_feature_flag_off_bug_super(self):
1400+ with celebrity_logged_in('admin'):
1401+ admin = getUtility(ILaunchBag).user
1402+ self.target.setBugSupervisor(
1403+ self.regular_user, admin)
1404+ old_link, new_link = self._create_scenario(
1405+ self.regular_user, None)
1406+ self.assertEqual(None, old_link, self.contents)
1407+ self.assertEqual(None, new_link, self.contents)
1408+
1409+ def test_subscribe_link_feature_flag_on_bug_super(self):
1410+ with celebrity_logged_in('admin'):
1411+ admin = getUtility(ILaunchBag).user
1412+ self.target.setBugSupervisor(
1413+ self.regular_user, admin)
1414+ old_link, new_link = self._create_scenario(
1415+ self.regular_user, 'on')
1416+ self.assertEqual(None, old_link, self.contents)
1417+ self.assertNotEqual(None, new_link, self.contents)
1418+
1419+ def test_subscribe_link_feature_flag_off_admin(self):
1420+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1421+ old_link, new_link = self._create_scenario(
1422+ admin, None)
1423+ self.assertEqual(None, old_link, self.contents)
1424+ self.assertEqual(None, new_link, self.contents)
1425+
1426+ def test_subscribe_link_feature_flag_on_admin(self):
1427+ from lp.testing.sampledata import ADMIN_EMAIL
1428+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1429+ old_link, new_link = self._create_scenario(
1430+ admin, 'on')
1431+ self.assertEqual(None, old_link, self.contents)
1432+ self.assertNotEqual(None, new_link, self.contents)
1433+
1434+
1435+class TestDistroBugsStructSubs(TestDistroViewStructSubs):
1436+ """Test structural subscriptions on the distro bugs view."""
1437+
1438+ rootsite = 'bugs'
1439+ view = '+bugs-index'
1440+
1441+ def test_subscribe_link_feature_flag_off_owner(self):
1442+ old_link, new_link = self._create_scenario(
1443+ self.target.owner, None)
1444+ self.assertNotEqual(None, old_link, self.contents)
1445+ self.assertEqual(None, new_link, self.contents)
1446+
1447+ def test_subscribe_link_feature_flag_on_owner(self):
1448+ old_link, new_link = self._create_scenario(
1449+ self.target.owner, 'on')
1450+ self.assertEqual(None, old_link, self.contents)
1451+ self.assertNotEqual(None, new_link, self.contents)
1452+
1453+ def test_subscribe_link_feature_flag_off_user(self):
1454+ old_link, new_link = self._create_scenario(
1455+ self.regular_user, None)
1456+ self.assertNotEqual(None, old_link, self.contents)
1457+ self.assertEqual(None, new_link, self.contents)
1458+
1459+ def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
1460+ old_link, new_link = self._create_scenario(
1461+ self.regular_user, 'on')
1462+ self.assertEqual(None, old_link, self.contents)
1463+ self.assertNotEqual(None, new_link, self.contents)
1464+
1465+ def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
1466+ with celebrity_logged_in('admin'):
1467+ admin = getUtility(ILaunchBag).user
1468+ supervisor = self.factory.makePerson()
1469+ self.target.setBugSupervisor(
1470+ supervisor, admin)
1471+ old_link, new_link = self._create_scenario(
1472+ self.regular_user, 'on')
1473+ self.assertEqual(None, old_link, self.contents)
1474+ self.assertEqual(None, new_link, self.contents)
1475+
1476+ def test_subscribe_link_feature_flag_off_anonymous(self):
1477+ old_link, new_link = self._create_scenario(
1478+ ANONYMOUS, None)
1479+ self.assertNotEqual(None, old_link, self.contents)
1480+ self.assertEqual(None, new_link, self.contents)
1481+
1482+ def test_subscribe_link_feature_flag_on_anonymous(self):
1483+ old_link, new_link = self._create_scenario(
1484+ ANONYMOUS, 'on')
1485+ self.assertEqual(None, old_link, self.contents)
1486+ self.assertEqual(None, new_link, self.contents)
1487+
1488+ def test_subscribe_link_feature_flag_off_bug_super(self):
1489+ with celebrity_logged_in('admin'):
1490+ admin = getUtility(ILaunchBag).user
1491+ self.target.setBugSupervisor(
1492+ self.regular_user, admin)
1493+ old_link, new_link = self._create_scenario(
1494+ self.regular_user, None)
1495+ self.assertNotEqual(None, old_link, self.contents)
1496+ self.assertEqual(None, new_link, self.contents)
1497+
1498+ def test_subscribe_link_feature_flag_on_bug_super(self):
1499+ with celebrity_logged_in('admin'):
1500+ admin = getUtility(ILaunchBag).user
1501+ self.target.setBugSupervisor(
1502+ self.regular_user, admin)
1503+ old_link, new_link = self._create_scenario(
1504+ self.regular_user, 'on')
1505+ self.assertEqual(None, old_link, self.contents)
1506+ self.assertNotEqual(None, new_link, self.contents)
1507+
1508+ def test_subscribe_link_feature_flag_off_admin(self):
1509+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1510+ old_link, new_link = self._create_scenario(
1511+ admin, None)
1512+ self.assertNotEqual(None, old_link, self.contents)
1513+ self.assertEqual(None, new_link, self.contents)
1514+
1515+ def test_subscribe_link_feature_flag_on_admin(self):
1516+ from lp.testing.sampledata import ADMIN_EMAIL
1517+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1518+ old_link, new_link = self._create_scenario(
1519+ admin, 'on')
1520+ self.assertEqual(None, old_link, self.contents)
1521+ self.assertNotEqual(None, new_link, self.contents)
1522+
1523+
1524+class TestDistroMilestoneViewStructSubs(TestDistroViewStructSubs):
1525+ """Test structural subscriptions on the distro milestones."""
1526+
1527+ def setUp(self):
1528+ super(TestDistroMilestoneViewStructSubs, self).setUp()
1529+ self.distro = self.target
1530+ self.target = self.factory.makeMilestone(distribution=self.distro)
1531+
1532+ def test_subscribe_link_feature_flag_off_owner(self):
1533+ old_link, new_link = self._create_scenario(
1534+ self.distro.owner, None)
1535+ self.assertNotEqual(None, old_link, self.contents)
1536+ self.assertEqual(None, new_link, self.contents)
1537+
1538+ def test_subscribe_link_feature_flag_on_owner(self):
1539+ old_link, new_link = self._create_scenario(
1540+ self.distro.owner, 'on')
1541+ self.assertEqual(None, old_link, self.contents)
1542+ self.assertNotEqual(None, new_link, self.contents)
1543+
1544+ def test_subscribe_link_feature_flag_off_user(self):
1545+ old_link, new_link = self._create_scenario(
1546+ self.regular_user, None)
1547+ self.assertNotEqual(None, old_link, self.contents)
1548+ self.assertEqual(None, new_link, self.contents)
1549+
1550+ def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
1551+ old_link, new_link = self._create_scenario(
1552+ self.regular_user, 'on')
1553+ self.assertEqual(None, old_link, self.contents)
1554+ self.assertNotEqual(None, new_link, self.contents)
1555+
1556+ def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
1557+ with celebrity_logged_in('admin'):
1558+ admin = getUtility(ILaunchBag).user
1559+ supervisor = self.factory.makePerson()
1560+ self.distro.setBugSupervisor(
1561+ supervisor, admin)
1562+ old_link, new_link = self._create_scenario(
1563+ self.regular_user, 'on')
1564+ self.assertEqual(None, old_link, self.contents)
1565+ self.assertNotEqual(None, new_link, self.contents)
1566+
1567+ def test_subscribe_link_feature_flag_off_anonymous(self):
1568+ old_link, new_link = self._create_scenario(
1569+ ANONYMOUS, None)
1570+ self.assertNotEqual(None, old_link, self.contents)
1571+ self.assertEqual(None, new_link, self.contents)
1572+
1573+ def test_subscribe_link_feature_flag_on_anonymous(self):
1574+ old_link, new_link = self._create_scenario(
1575+ ANONYMOUS, 'on')
1576+ self.assertEqual(None, old_link, self.contents)
1577+ self.assertEqual(None, new_link, self.contents)
1578+
1579+ def test_subscribe_link_feature_flag_off_bug_super(self):
1580+ with celebrity_logged_in('admin'):
1581+ admin = getUtility(ILaunchBag).user
1582+ self.distro.setBugSupervisor(
1583+ self.regular_user, admin)
1584+ old_link, new_link = self._create_scenario(
1585+ self.regular_user, None)
1586+ self.assertNotEqual(None, old_link, self.contents)
1587+ self.assertEqual(None, new_link, self.contents)
1588+
1589+ def test_subscribe_link_feature_flag_on_bug_super(self):
1590+ with celebrity_logged_in('admin'):
1591+ admin = getUtility(ILaunchBag).user
1592+ self.distro.setBugSupervisor(
1593+ self.regular_user, admin)
1594+ old_link, new_link = self._create_scenario(
1595+ self.regular_user, 'on')
1596+ self.assertEqual(None, old_link, self.contents)
1597+ self.assertNotEqual(None, new_link, self.contents)
1598+
1599+ def test_subscribe_link_feature_flag_off_admin(self):
1600+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1601+ old_link, new_link = self._create_scenario(
1602+ admin, None)
1603+ self.assertNotEqual(None, old_link, self.contents)
1604+ self.assertEqual(None, new_link, self.contents)
1605+
1606+ def test_subscribe_link_feature_flag_on_admin(self):
1607+ from lp.testing.sampledata import ADMIN_EMAIL
1608+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1609+ old_link, new_link = self._create_scenario(
1610+ admin, 'on')
1611+ self.assertEqual(None, old_link, self.contents)
1612+ self.assertNotEqual(None, new_link, self.contents)
1613+
1614+
1615+class TestProductMilestoneViewStructSubs(TestDistroViewStructSubs):
1616+ """Test structural subscriptions on the product milestones."""
1617+
1618+ def setUp(self):
1619+ super(TestProductMilestoneViewStructSubs, self).setUp()
1620+ self.product = self.factory.makeProduct()
1621+ with person_logged_in(self.product.owner):
1622+ self.product.official_malone = True
1623+ self.regular_user = self.factory.makePerson()
1624+ self.target = self.factory.makeMilestone(product=self.product)
1625+
1626+ def test_subscribe_link_feature_flag_off_owner(self):
1627+ old_link, new_link = self._create_scenario(
1628+ self.product.owner, None)
1629+ self.assertNotEqual(None, old_link, self.contents)
1630+ self.assertEqual(None, new_link, self.contents)
1631+
1632+ def test_subscribe_link_feature_flag_on_owner(self):
1633+ old_link, new_link = self._create_scenario(
1634+ self.product.owner, 'on')
1635+ self.assertEqual(None, old_link, self.contents)
1636+ self.assertNotEqual(None, new_link, self.contents)
1637+
1638+ def test_subscribe_link_feature_flag_off_user(self):
1639+ old_link, new_link = self._create_scenario(
1640+ self.regular_user, None)
1641+ self.assertNotEqual(None, old_link, self.contents)
1642+ self.assertEqual(None, new_link, self.contents)
1643+
1644+ # There are no special bug supervisor rules for products.
1645+ test_subscribe_link_feature_flag_on_user_no_bug_super = None
1646+ test_subscribe_link_feature_flag_on_user_with_bug_super = None
1647+ test_subscribe_link_feature_flag_off_bug_super = None
1648+ test_subscribe_link_feature_flag_on_bug_super = None
1649+
1650+ def test_subscribe_link_feature_flag_off_anonymous(self):
1651+ old_link, new_link = self._create_scenario(
1652+ ANONYMOUS, None)
1653+ self.assertNotEqual(None, old_link, self.contents)
1654+ self.assertEqual(None, new_link, self.contents)
1655+
1656+ def test_subscribe_link_feature_flag_on_anonymous(self):
1657+ old_link, new_link = self._create_scenario(
1658+ ANONYMOUS, 'on')
1659+ self.assertEqual(None, old_link, self.contents)
1660+ self.assertEqual(None, new_link, self.contents)
1661+
1662+ def test_subscribe_link_feature_flag_off_admin(self):
1663+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1664+ old_link, new_link = self._create_scenario(
1665+ admin, None)
1666+ self.assertNotEqual(None, old_link, self.contents)
1667+ self.assertEqual(None, new_link, self.contents)
1668+
1669+ def test_subscribe_link_feature_flag_on_admin(self):
1670+ from lp.testing.sampledata import ADMIN_EMAIL
1671+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
1672+ old_link, new_link = self._create_scenario(
1673+ admin, 'on')
1674+ self.assertEqual(None, old_link, self.contents)
1675+ self.assertNotEqual(None, new_link, self.contents)
1676+
1677+
1678+class TestProductSeriesMilestoneViewStructSubs(
1679+ TestProductMilestoneViewStructSubs):
1680+ """Test structural subscriptions on the product series milestones."""
1681+
1682+ def setUp(self):
1683+ super(TestProductSeriesMilestoneViewStructSubs, self).setUp()
1684+ self.productseries = self.factory.makeProductSeries()
1685+ with person_logged_in(self.productseries.product.owner):
1686+ self.productseries.product.official_malone = True
1687+ self.regular_user = self.factory.makePerson()
1688+ self.target = self.factory.makeMilestone(
1689+ productseries=self.productseries)
1690+
1691+
1692+def test_suite():
1693+ """Return the `IStructuralSubscriptionTarget` TestSuite."""
1694+
1695+ # Manually construct the test suite to avoid having tests from the base
1696+ # class _TestStructSubs run.
1697+ suite = unittest.TestSuite()
1698+ suite.addTest(unittest.makeSuite(TestProductViewStructSubs))
1699+ suite.addTest(unittest.makeSuite(TestProductBugsStructSubs))
1700+ suite.addTest(unittest.makeSuite(TestProductSeriesViewStructSubs))
1701+ suite.addTest(unittest.makeSuite(TestProductSeriesBugsStructSubs))
1702+ suite.addTest(unittest.makeSuite(TestProjectGroupViewStructSubs))
1703+ suite.addTest(unittest.makeSuite(TestProjectGroupBugsStructSubs))
1704+ suite.addTest(unittest.makeSuite(
1705+ TestDistributionSourcePackageViewStructSubs))
1706+ suite.addTest(unittest.makeSuite(
1707+ TestDistributionSourcePackageBugsStructSubs))
1708+ suite.addTest(unittest.makeSuite(TestDistroViewStructSubs))
1709+ suite.addTest(unittest.makeSuite(TestDistroBugsStructSubs))
1710+ suite.addTest(unittest.makeSuite(TestDistroMilestoneViewStructSubs))
1711+ suite.addTest(unittest.makeSuite(TestProductMilestoneViewStructSubs))
1712+ suite.addTest(unittest.makeSuite(
1713+ TestProductSeriesMilestoneViewStructSubs))
1714+ return suite
1715
1716=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
1717--- lib/lp/registry/javascript/structural-subscription.js 2011-03-29 13:02:44 +0000
1718+++ lib/lp/registry/javascript/structural-subscription.js 2011-03-29 22:36:23 +0000
1719@@ -80,7 +80,7 @@
1720 tags: [],
1721 find_all_tags: false,
1722 importances: [],
1723- statuses: [],
1724+ statuses: []
1725 };
1726
1727 // Set the notification level.
1728
1729=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
1730--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-29 13:02:44 +0000
1731+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-29 22:36:23 +0000
1732@@ -87,7 +87,7 @@
1733 LP.cache.statuses = [];
1734
1735 this.configuration = {
1736- content_box: content_box_id,
1737+ content_box: content_box_id
1738 };
1739 this.content_node = create_test_node();
1740 Y.one('body').appendChild(this.content_node);
1741@@ -631,7 +631,7 @@
1742 TestBugFilter.prototype = {
1743 'getAttrs': function () {
1744 return {};
1745- },
1746+ }
1747 };
1748
1749 // Now we need an lp_client that will appear to succesfully create
1750@@ -648,7 +648,7 @@
1751 'patch': function(uri, representation, config, headers) {
1752 config.on.failure(true, {'status':400});
1753 patch_failed = true;
1754- },
1755+ }
1756 };
1757 module.lp_client = new TestClient();
1758
1759@@ -661,7 +661,7 @@
1760 // Delete should have been called and the patch has failed.
1761 Assert.isTrue(delete_called);
1762 Assert.isTrue(patch_failed);
1763- },
1764+ }
1765
1766 }));
1767
1768@@ -707,7 +707,7 @@
1769 var form_data = {
1770 name: ['filter description'],
1771 events: [],
1772- filters: [],
1773+ filters: []
1774 };
1775 var patch_data = module._extract_form_data(form_data);
1776 Assert.areEqual(patch_data.description, form_data.name[0]);
1777@@ -719,7 +719,7 @@
1778 var form_data = {
1779 name: [' filter description '],
1780 events: [],
1781- filters: [],
1782+ filters: []
1783 };
1784 var patch_data = module._extract_form_data(form_data);
1785 Assert.areEqual('filter description', patch_data.description);
1786@@ -729,7 +729,7 @@
1787 var form_data = {
1788 name: [],
1789 events: ['added-or-closed'],
1790- filters: [],
1791+ filters: []
1792 };
1793 var patch_data = module._extract_form_data(form_data);
1794 Assert.areEqual(
1795@@ -740,7 +740,7 @@
1796 var form_data = {
1797 name: [],
1798 events: [],
1799- filters: ['filter-comments'],
1800+ filters: ['filter-comments']
1801 };
1802 var patch_data = module._extract_form_data(form_data);
1803 Assert.areEqual(
1804@@ -751,7 +751,7 @@
1805 var form_data = {
1806 name: [],
1807 events: [],
1808- filters: [],
1809+ filters: []
1810 };
1811 var patch_data = module._extract_form_data(form_data);
1812 Assert.areEqual(
1813@@ -766,7 +766,7 @@
1814 tags: ['one two THREE'],
1815 tag_match: [''],
1816 importances: [],
1817- statuses: [],
1818+ statuses: []
1819 };
1820 var patch_data = module._extract_form_data(form_data);
1821 // Note that the tags are converted to lower case.
1822@@ -782,7 +782,7 @@
1823 tags: ['tag'],
1824 tag_match: ['match-all'],
1825 importances: [],
1826- statuses: [],
1827+ statuses: []
1828 };
1829 var patch_data = module._extract_form_data(form_data);
1830 Assert.isTrue(patch_data.find_all_tags);
1831@@ -796,7 +796,7 @@
1832 tags: ['tag'],
1833 tag_match: [],
1834 importances: [],
1835- statuses: [],
1836+ statuses: []
1837 };
1838 var patch_data = module._extract_form_data(form_data);
1839 Assert.isFalse(patch_data.find_all_tags);
1840@@ -813,16 +813,16 @@
1841 tags: ['tag'],
1842 tag_match: ['match-all'],
1843 importances: ['importance1'],
1844- statuses: ['status1'],
1845+ statuses: ['status1']
1846 };
1847 var patch_data = module._extract_form_data(form_data);
1848 // Since advanced-filter isn't set, all the advanced values should
1849 // be empty/false despite the form values.
1850 Assert.isFalse(patch_data.find_all_tags);
1851- ArrayAssert.isEmpty(patch_data.tags)
1852- ArrayAssert.isEmpty(patch_data.importances)
1853- ArrayAssert.isEmpty(patch_data.statuses)
1854- },
1855+ ArrayAssert.isEmpty(patch_data.tags);
1856+ ArrayAssert.isEmpty(patch_data.importances);
1857+ ArrayAssert.isEmpty(patch_data.statuses);
1858+ }
1859
1860 }));
1861
1862
1863=== modified file 'lib/lp/registry/templates/distribution-index.pt'
1864--- lib/lp/registry/templates/distribution-index.pt 2010-10-10 21:54:16 +0000
1865+++ lib/lp/registry/templates/distribution-index.pt 2011-03-29 22:36:23 +0000
1866@@ -5,6 +5,22 @@
1867 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1868 metal:use-macro="view/macro:page/main_side"
1869 i18n:domain="launchpad">
1870+
1871+ <head>
1872+ <tal:head-epilogue metal:fill-slot="head_epilogue">
1873+ <script type="text/javascript"
1874+ tal:condition="
1875+ request/features/malone.advanced-structural-subscriptions.enabled">
1876+ LPS.use('lp.registry.structural_subscription', function(Y) {
1877+ var module = Y.lp.registry.structural_subscription;
1878+ Y.on('domready', function() {
1879+ module.setup({content_box: "#structural-subscription-content-box"});
1880+ });
1881+ });
1882+ </script>
1883+ </tal:head-epilogue>
1884+ </head>
1885+
1886 <body>
1887 <tal:heading metal:fill-slot="heading">
1888 <h1 tal:content="context/title">project title</h1>
1889@@ -69,6 +85,9 @@
1890
1891 <div tal:replace="structure context/@@+portlet-coming-sprints" />
1892 </div>
1893+ <div class="yui-u">
1894+ <div id="structural-subscription-content-box"></div>
1895+ </div>
1896 </div>
1897 </tal:main>
1898
1899
1900=== modified file 'lib/lp/registry/templates/distributionsourcepackage-index.pt'
1901--- lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-03-23 16:28:51 +0000
1902+++ lib/lp/registry/templates/distributionsourcepackage-index.pt 2011-03-29 22:36:23 +0000
1903@@ -16,6 +16,16 @@
1904 tal:attributes="src string:${lp_js}/soyuz/base.js"></script>
1905 </tal:archive_js>
1906 </tal:devmode>
1907+ <script type="text/javascript"
1908+ tal:condition="
1909+ request/features/malone.advanced-structural-subscriptions.enabled">
1910+ LPS.use('lp.registry.structural_subscription', function(Y) {
1911+ var module = Y.lp.registry.structural_subscription;
1912+ Y.on('domready', function() {
1913+ module.setup({content_box: "#structural-subscription-content-box"});
1914+ });
1915+ });
1916+ </script>
1917 </metal:block>
1918
1919 <tal:side metal:fill-slot="side">
1920@@ -28,6 +38,9 @@
1921 </tal:side>
1922
1923 <tal:main metal:fill-slot="main">
1924+ <div class="yui-u">
1925+ <div id="structural-subscription-content-box"></div>
1926+ </div>
1927 <div class="top-portlet" id="bugs-and-questions-summary"
1928 tal:define="newbugs context/new_bugtasks/count;
1929 open_questions view/open_questions/count">
1930@@ -169,7 +182,7 @@
1931 <tr tal:attributes="id string:pub${pubid}" style="display: none">
1932 <td colspan="3">
1933 <div class="package-details"
1934- tal:attributes="id string:pub${pubid}-container" />
1935+ tal:attributes="id string:pub${pubid}-container"></div>
1936 </td>
1937 </tr>
1938
1939
1940=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
1941--- lib/lp/registry/templates/distroseries-index.pt 2010-10-10 21:54:16 +0000
1942+++ lib/lp/registry/templates/distroseries-index.pt 2011-03-29 22:36:23 +0000
1943@@ -5,6 +5,7 @@
1944 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1945 metal:use-macro="view/macro:page/main_side"
1946 i18n:domain="launchpad">
1947+
1948 <body>
1949 <metal:block fill-slot="head_epilogue">
1950 <metal:yui-dependencies
1951@@ -12,6 +13,16 @@
1952 <script id="milestone-script" type="text/javascript"
1953 tal:condition="context/menu:overview/create_milestone/enabled"
1954 tal:content="view/register_milestone_script"></script>
1955+ <script type="text/javascript"
1956+ tal:condition="
1957+ request/features/malone.advanced-structural-subscriptions.enabled">
1958+ LPS.use('lp.registry.structural_subscription', function(Y) {
1959+ var module = Y.lp.registry.structural_subscription;
1960+ Y.on('domready', function() {
1961+ module.setup({content_box: "#structural-subscription-content-box"});
1962+ });
1963+ });
1964+ </script>
1965 </metal:block>
1966
1967 <tal:heading metal:fill-slot="heading">
1968@@ -108,12 +119,16 @@
1969 </ul>
1970 </div>
1971 </div>
1972+ <div class="yui-u">
1973+ <div id="structural-subscription-content-box"></div>
1974+ </div>
1975 </div>
1976 </div>
1977
1978 <tal:side metal:fill-slot="side"
1979 define="overview_menu context/menu:overview">
1980- <div id="global-actions" class="portlet">
1981+ <div id="global-actions" class="portlet"
1982+ condition="overview_menu/subscribe_to_bug_mail/enabled|overview_menu/subscribe/enabled|nothing">
1983 <ul>
1984 <li tal:condition="overview_menu/edit/enabled">
1985 <a tal:replace="structure overview_menu/edit/fmt:link" />
1986@@ -124,9 +139,12 @@
1987 <li tal:condition="overview_menu/reassign/enabled">
1988 <a tal:replace="structure overview_menu/reassign/fmt:link" />
1989 </li>
1990- <li>
1991+ <li tal:condition="overview_menu/subscribe/enabled|nothing">
1992 <a tal:replace="structure overview_menu/subscribe/fmt:link" />
1993 </li>
1994+ <li tal:condition="overview_menu/subscribe_to_bug_mail/enabled|nothing">
1995+ <a tal:replace="structure overview_menu/subscribe_to_bug_mail/fmt:link" />
1996+ </li>
1997 </ul>
1998 </div>
1999
2000
2001=== modified file 'lib/lp/registry/templates/milestone-index.pt'
2002--- lib/lp/registry/templates/milestone-index.pt 2011-03-04 00:08:20 +0000
2003+++ lib/lp/registry/templates/milestone-index.pt 2011-03-29 22:36:23 +0000
2004@@ -6,14 +6,24 @@
2005 metal:use-macro="view/macro:page/main_side"
2006 i18n:domain="launchpad">
2007
2008- <tal:css metal:fill-slot="head_epilogue"
2009+ <tal:head-epilogue metal:fill-slot="head_epilogue"
2010 condition="view/is_project_milestone">
2011 <style id="hide-side-portlets" type="text/css">
2012 .side {
2013 background: #fff;
2014 }
2015 </style>
2016- </tal:css>
2017+ <script type="text/javascript"
2018+ tal:condition="
2019+ request/features/malone.advanced-structural-subscriptions.enabled">
2020+ LPS.use('lp.registry.structural_subscription', function(Y) {
2021+ var module = Y.lp.registry.structural_subscription;
2022+ Y.on('domready', function() {
2023+ module.setup({content_box: "#structural-subscription-content-box"});
2024+ });
2025+ });
2026+ </script>
2027+ </tal:head-epilogue>
2028
2029 <body>
2030 <tal:heading metal:fill-slot="heading">
2031@@ -308,6 +318,9 @@
2032 </li>
2033 </ul>
2034 </div>
2035+ <div class="yui-u">
2036+ <div id="structural-subscription-content-box"></div>
2037+ </div>
2038 </div>
2039
2040 <tal:side metal:fill-slot="side">
2041
2042=== modified file 'lib/lp/registry/templates/product-index.pt'
2043--- lib/lp/registry/templates/product-index.pt 2011-03-23 15:55:44 +0000
2044+++ lib/lp/registry/templates/product-index.pt 2011-03-29 22:36:23 +0000
2045@@ -34,9 +34,9 @@
2046
2047 <script type="text/javascript"
2048 tal:condition="
2049- request/features/advanced-structural-subscriptions.enabled">
2050+ request/features/malone.advanced-structural-subscriptions.enabled">
2051 LPS.use('lp.registry.structural_subscription', function(Y) {
2052- module = Y.lp.registry.structural_subscription;
2053+ var module = Y.lp.registry.structural_subscription;
2054 Y.on('domready', function() {
2055 module.setup({content_box: "#structural-subscription-content-box"});
2056 });
2057
2058=== modified file 'lib/lp/registry/templates/productseries-index.pt'
2059--- lib/lp/registry/templates/productseries-index.pt 2011-03-23 16:28:51 +0000
2060+++ lib/lp/registry/templates/productseries-index.pt 2011-03-29 22:36:23 +0000
2061@@ -14,6 +14,16 @@
2062 <script id="milestone-script" type="text/javascript"
2063 tal:condition="context/menu:overview/create_milestone/enabled"
2064 tal:content="view/register_milestone_script"></script>
2065+ <script type="text/javascript"
2066+ tal:condition="
2067+ request/features/malone.advanced-structural-subscriptions.enabled">
2068+ LPS.use('lp.registry.structural_subscription', function(Y) {
2069+ var module = Y.lp.registry.structural_subscription;
2070+ Y.on('domready', function() {
2071+ module.setup({content_box: "#structural-subscription-content-box"});
2072+ });
2073+ });
2074+ </script>
2075 </metal:block>
2076
2077 <tal:heading metal:fill-slot="heading">
2078@@ -178,12 +188,17 @@
2079 </ul>
2080 </div>
2081 </div>
2082+ <div class="yui-u">
2083+ <div id="structural-subscription-content-box"></div>
2084+ </div>
2085 </div>
2086 </div>
2087
2088 <tal:side metal:fill-slot="side"
2089- define="overview_menu context/menu:overview">
2090- <div id="global-actions" class="portlet">
2091+ define="overview_menu context/menu:overview;
2092+ feature_flag request/features/malone.advanced-structural-subscriptions.enabled">
2093+ <div id="global-actions" class="portlet"
2094+ tal:condition="overview_menu/subscribe_to_bug_mail/enabled|overview_menu/subscribe/enabled">
2095 <ul>
2096 <li tal:condition="overview_menu/edit/enabled">
2097 <a tal:replace="structure overview_menu/edit/fmt:link" />
2098@@ -191,13 +206,21 @@
2099 <li tal:condition="overview_menu/delete/enabled">
2100 <a tal:replace="structure overview_menu/delete/fmt:link" />
2101 </li>
2102- <li>
2103- <a tal:replace="structure overview_menu/subscribe/fmt:link" />
2104- </li>
2105+ <tal:advanced-structural-subscriptions
2106+ condition="feature_flag">
2107+ <li tal:condition="overview_menu/subscribe_to_bug_mail/enabled|nothing">
2108+ <a tal:replace="structure overview_menu/subscribe_to_bug_mail/fmt:link" />
2109+ </li>
2110+ </tal:advanced-structural-subscriptions>
2111+ <tal:not-advanced-structural-subscriptions
2112+ condition="not: feature_flag">
2113+ <li tal:condition="overview_menu/subscribe/enabled|nothing">
2114+ <a tal:replace="structure overview_menu/subscribe/fmt:link" />
2115+ </li>
2116+ </tal:not-advanced-structural-subscriptions>
2117 </ul>
2118 </div>
2119
2120-
2121 <div id="downloads" class="top-portlet downloads"
2122 tal:define="release view/latest_release_with_download_files">
2123 <h2>Downloads</h2>
2124
2125=== modified file 'lib/lp/registry/templates/project-index.pt'
2126--- lib/lp/registry/templates/project-index.pt 2010-10-10 21:54:16 +0000
2127+++ lib/lp/registry/templates/project-index.pt 2011-03-29 22:36:23 +0000
2128@@ -6,6 +6,22 @@
2129 metal:use-macro="view/macro:page/main_side"
2130 i18n:domain="launchpad"
2131 >
2132+
2133+ <head>
2134+ <tal:head-epilogue metal:fill-slot="head_epilogue">
2135+ <script type="text/javascript"
2136+ tal:condition="
2137+ request/features/malone.advanced-structural-subscriptions.enabled">
2138+ LPS.use('lp.registry.structural_subscription', function(Y) {
2139+ var module = Y.lp.registry.structural_subscription;
2140+ Y.on('domready', function() {
2141+ module.setup({content_box: "#structural-subscription-content-box"});
2142+ });
2143+ });
2144+ </script>
2145+ </tal:head-epilogue>
2146+ </head>
2147+
2148 <body>
2149 <tal:registering metal:fill-slot="registering">
2150 Registered
2151@@ -127,6 +143,9 @@
2152 <tal:sprints content="structure context/@@+portlet-coming-sprints" />
2153 </tal:has-few-project>
2154 </div>
2155+ <div class="yui-u">
2156+ <div id="structural-subscription-content-box"></div>
2157+ </div>
2158 </div>
2159 </tal:main>
2160
2161
2162=== modified file 'lib/lp/services/features/flags.py'
2163--- lib/lp/services/features/flags.py 2011-03-28 05:33:33 +0000
2164+++ lib/lp/services/features/flags.py 2011-03-29 22:36:23 +0000
2165@@ -53,6 +53,10 @@
2166 'boolean',
2167 'Enables advanced bug subscription features.',
2168 ''),
2169+ ('malone.advanced-structural-subscriptions.enabled',
2170+ 'boolean',
2171+ 'Enables advanced structural subscriptions',
2172+ ''),
2173 ('malone.disable_targetnamesearch',
2174 'boolean',
2175 'If true, disables consultation of target names during bug text search.',