← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Brad Crittenden has proposed merging lp:~bac/launchpad/accordion-client-2 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~bac/launchpad/accordion-client-2/+merge/55361

= 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
-- 
https://code.launchpad.net/~bac/launchpad/accordion-client-2/+merge/55361
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/accordion-client-2 into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
--- lib/lp/bugs/browser/bugsubscription.py	2011-03-29 00:11:57 +0000
+++ lib/lp/bugs/browser/bugsubscription.py	2011-03-29 15:31:14 +0000
@@ -40,7 +40,7 @@
     )
 from lp.bugs.browser.bug import BugViewMixin
 from lp.bugs.browser.structuralsubscription import (
-    StructuralSubscriptionJSMixin,
+    expose_structural_subscription_data_to_js,
     )
 from lp.bugs.enum import BugNotificationLevel, HIDDEN_BUG_NOTIFICATION_LEVELS
 from lp.bugs.interfaces.bugsubscription import IBugSubscription
@@ -578,9 +578,14 @@
         return 'subscriber-%s' % self.subscription.person.id
 
 
-class BugSubscriptionListView(StructuralSubscriptionJSMixin, LaunchpadView):
+class BugSubscriptionListView(LaunchpadView):
     """A view to show all a person's subscriptions to a bug."""
 
+    def initialize(self):
+        super(BugSubscriptionListView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user, self.subscriptions)
+
     @property
     def subscriptions(self):
         return get_structural_subscriptions_for_bug(

=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2011-03-23 19:20:03 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2011-03-29 15:31:14 +0000
@@ -99,7 +99,7 @@
 from lp.bugs.browser.bugrole import BugRoleMixin
 from lp.bugs.browser.bugtask import BugTaskSearchListingView
 from lp.bugs.browser.structuralsubscription import (
-    StructuralSubscriptionJSMixin,
+    expose_structural_subscription_data_to_js,
     )
 from lp.bugs.browser.widgets.bug import (
     BugTagsWidget,
@@ -1308,10 +1308,12 @@
         return 'Bugs in %s' % self.context.title
 
     def initialize(self):
-        BugTaskSearchListingView.initialize(self)
+        super(BugTargetBugsView, self).initialize()
         bug_statuses_to_show = list(UNRESOLVED_BUGTASK_STATUSES)
         if IDistroSeries.providedBy(self.context):
             bug_statuses_to_show.append(BugTaskStatus.FIXRELEASED)
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
 
     @property
     def can_have_external_bugtracker(self):
@@ -1565,9 +1567,14 @@
         return ProxiedLibraryFileAlias(patch.libraryfile, patch).http_url
 
 
-class TargetSubscriptionView(StructuralSubscriptionJSMixin, LaunchpadView):
+class TargetSubscriptionView(LaunchpadView):
     """A view to show all a person's structural subscriptions to a target."""
 
+    def initialize(self):
+        super(TargetSubscriptionView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user, self.subscriptions)
+
     @property
     def subscriptions(self):
         return get_structural_subscriptions_for_target(

=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py	2011-03-25 15:33:51 +0000
+++ lib/lp/bugs/browser/bugtask.py	2011-03-29 15:31:14 +0000
@@ -195,6 +195,9 @@
     BugTextView,
     BugViewMixin,
     )
+from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
+    )
 from lp.bugs.browser.bugcomment import (
     build_comments_from_chunks,
     group_comments_with_activity,
@@ -275,12 +278,11 @@
 from lp.services.fields import PersonChoice
 from lp.services.propertycache import (
     cachedproperty,
-    get_property_cache,
     )
 
 
 DISPLAY_BUG_STATUS_FOR_PATCHES = {
-    BugTaskStatus.NEW:  True,
+    BugTaskStatus.NEW: True,
     BugTaskStatus.INCOMPLETE: True,
     BugTaskStatus.INVALID: False,
     BugTaskStatus.WONTFIX: False,
@@ -290,7 +292,7 @@
     BugTaskStatus.FIXCOMMITTED: True,
     BugTaskStatus.FIXRELEASED: False,
     BugTaskStatus.UNKNOWN: False,
-    BugTaskStatus.EXPIRED: False
+    BugTaskStatus.EXPIRED: False,
     }
 
 
@@ -2205,11 +2207,6 @@
         return Link(
             '+securitycontact', 'Change security contact', icon='edit')
 
-    def subscribe(self):
-        user = getUtility(ILaunchBag).user
-        if self.context.userCanAlterBugSubscription(user):
-            return Link('+subscribe', 'Subscribe to bug mail', icon='edit')
-
     def nominations(self):
         return Link('+nominations', 'Review nominations', icon='bug')
 
@@ -2346,6 +2343,9 @@
         # needing validation is already available internally to self.
         self._validate(None, {})
 
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
+
     @property
     def columns_to_show(self):
         """Returns a sequence of column names to be shown in the listing."""
@@ -3196,7 +3196,7 @@
         # Hint to optimize when there are many bugtasks.
         view.many_bugtasks = self.many_bugtasks
         return view
-    
+
     def getBugTaskAndNominationViews(self):
         """Return the IBugTasks and IBugNominations views for this bug.
 

=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py	2011-03-24 15:31:53 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py	2011-03-29 15:31:14 +0000
@@ -5,9 +5,9 @@
 
 __all__ = [
     'expose_enum_to_js',
+    'expose_structural_subscription_data_to_js',
     'expose_user_administered_teams_to_js',
     'expose_user_subscriptions_to_js',
-    'StructuralSubscriptionJSMixin',
     'StructuralSubscriptionMenuMixin',
     'StructuralSubscriptionTargetTraversalMixin',
     'StructuralSubscriptionView',
@@ -33,7 +33,10 @@
 from zope.traversing.browser import absoluteURL
 
 from canonical.launchpad.webapp.authorization import check_permission
-from canonical.launchpad.webapp.menu import Link
+from canonical.launchpad.webapp.menu import (
+    enabled_with_permission,
+    Link,
+    )
 from canonical.launchpad.webapp.publisher import (
     canonical_url,
     LaunchpadView,
@@ -329,6 +332,14 @@
 class StructuralSubscriptionMenuMixin:
     """Mix-in class providing the subscription add/edit menu link."""
 
+    def _getSST(self):
+        if IStructuralSubscriptionTarget.providedBy(self.context):
+            sst = self.context
+        else:
+            # self.context is a view, and the target is its context
+            sst = self.context.context
+        return sst
+
     def subscribe(self):
         """The subscribe menu link.
 
@@ -337,11 +348,7 @@
         and displays the edit icon. Otherwise, the link offers to subscribe
         and displays the add icon.
         """
-        if IStructuralSubscriptionTarget.providedBy(self.context):
-            sst = self.context
-        else:
-            # self.context is a view, and the target is its context
-            sst = self.context.context
+        sst = self._getSST()
 
         # ProjectGroup milestones aren't really structural subscription
         # targets as they're not real milestones, so you can't subscribe to
@@ -360,6 +367,25 @@
         else:
             return Link('+subscribe', text, icon=icon, enabled=enabled)
 
+    @enabled_with_permission('launchpad.AnyPerson')
+    def subscribe_to_bug_mail(self):
+        sst = self._getSST()
+        enabled = sst.userCanAlterBugSubscription(self.user, self.user)
+        text = 'Subscribe to bug mail'
+        return Link('#', text, icon='add', hidden=True, enabled=enabled)
+
+
+def expose_structural_subscription_data_to_js(context, request,
+                                              user, subscriptions=None):
+    """Expose all of the data for a structural subscription to JavaScript."""
+    expose_user_administered_teams_to_js(request, user)
+    expose_enum_to_js(request, BugTaskImportance, 'importances')
+    expose_enum_to_js(request, BugTaskStatus, 'statuses')
+    if subscriptions is None:
+        subscriptions = []
+    expose_user_subscriptions_to_js(
+        user, subscriptions, request)
+
 
 def expose_enum_to_js(request, enum, name):
     """Make a list of enum titles and value available to JavaScript."""
@@ -424,25 +450,6 @@
     IJSONRequestCache(request).objects['subscription_info'] = info
 
 
-class StructuralSubscriptionJSMixin:
-    """A mixin that exposes structural-subscription data in JS.
-
-    Descendants of this mixin must define a `subscriptions` property
-    that returns a list of the subscriptions to cache in the JS of the
-    page.
-    """
-
-    def initialize(self):
-        super(StructuralSubscriptionJSMixin, self).initialize()
-        expose_user_administered_teams_to_js(self.request, self.user)
-        expose_user_subscriptions_to_js(
-            self.user, self.subscriptions, self.request)
-        expose_enum_to_js(self.request, BugTaskImportance, 'importances')
-        expose_enum_to_js(self.request, BugTaskStatus, 'statuses')
-
-    subscriptions = None # Override this.
-
-
 class StructuralSubscribersPortletView(LaunchpadView):
     """A simple view for displaying the subscribers portlet."""
 

=== modified file 'lib/lp/bugs/templates/bug-subscription-list.pt'
--- lib/lp/bugs/templates/bug-subscription-list.pt	2011-03-25 21:00:51 +0000
+++ lib/lp/bugs/templates/bug-subscription-list.pt	2011-03-29 15:31:14 +0000
@@ -12,7 +12,9 @@
 
 <head>
   <tal:head-epilogue metal:fill-slot="head_epilogue">
-    <script type="text/javascript">
+    <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
       LPS.use('lp.registry.structural_subscription', function(Y) {
           module = Y.lp.registry.structural_subscription;
           Y.on('domready', function() {

=== modified file 'lib/lp/bugs/templates/buglisting-default.pt'
--- lib/lp/bugs/templates/buglisting-default.pt	2011-02-24 14:53:05 +0000
+++ lib/lp/bugs/templates/buglisting-default.pt	2011-03-29 15:31:14 +0000
@@ -10,6 +10,16 @@
 <metal:block fill-slot="head_epilogue">
   <meta condition="not: view/should_show_bug_information"
         name="robots" content="noindex,nofollow" />
+  <script type="text/javascript"
+      tal:condition="
+        request/features/malone.advanced-structural-subscriptions.enabled">
+    LPS.use('lp.registry.structural_subscription', function(Y) {
+        module = Y.lp.registry.structural_subscription;
+        Y.on('domready', function() {
+          module.setup({content_box: "#structural-subscription-content-box"});
+        });
+    });
+  </script>
 </metal:block>
 
 <body>
@@ -61,6 +71,9 @@
              use-macro="context/@@+bugtask-macros-tableview/advanced_search_form" />
         </tal:show_advanced_form>
 
+      <div class="yui-u">
+        <div id="structural-subscription-content-box"></div>
+      </div>
 
     </div>
     <div tal:condition="view/bug_tracking_usage/enumvalue:UNKNOWN"

=== modified file 'lib/lp/bugs/templates/bugtarget-bugs.pt'
--- lib/lp/bugs/templates/bugtarget-bugs.pt	2010-12-20 16:01:50 +0000
+++ lib/lp/bugs/templates/bugtarget-bugs.pt	2011-03-29 15:31:14 +0000
@@ -12,11 +12,22 @@
   <metal:block fill-slot="head_epilogue">
     <meta tal:condition="not: view/bug_tracking_usage/enumvalue:LAUNCHPAD"
           name="robots" content="noindex,nofollow" />
+    <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
+      LPS.use('lp.registry.structural_subscription', function(Y) {
+          module = Y.lp.registry.structural_subscription;
+          Y.on('domready', function() {
+            module.setup({content_box: "#structural-subscription-content-box"});
+          });
+      });
+    </script>
     <style type="text/css">
       p#more-hot-bugs {float:right; margin-top:7px;}
     </style>
-  </metal:block>
+</metal:block>
   <body>
+
     <tal:side metal:fill-slot="side"
               condition="view/bug_tracking_usage/enumvalue:LAUNCHPAD">
       <div id="involvement" class="portlet">
@@ -213,6 +224,10 @@
       </p>
     </div>
 
+    <div class="yui-u">
+      <div id="structural-subscription-content-box"></div>
+    </div>
+
     </div><!-- main -->
   </body>
 </html>

=== modified file 'lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt'
--- lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt	2010-08-16 23:28:48 +0000
+++ lib/lp/bugs/templates/bugtarget-portlet-bugfilters.pt	2011-03-29 15:31:14 +0000
@@ -10,21 +10,46 @@
     <tbody id="bugfilters-portlet-content"
            tal:content="structure context/@@+bugtarget-portlet-bugfilters-info" />
     <tbody tal:define="menu context/menu:bugs">
-      <tr tal:define="subscribe_link menu/subscribe|nothing"
-          tal:condition="python: subscribe_link and subscribe_link.enabled">
-        <td class="bugs-count" style="padding-top: 1em">
-          <a tal:attributes="href subscribe_link/url">
-            <img tal:attributes="src subscribe_link/icon_url" />
-          </a>
-        </td>
-        <td class="bugs-link">
-          <a tal:attributes="href subscribe_link/url"
-             tal:content="subscribe_link/escapedtext" />
-        </td>
-      </tr>
+
+      <tal:advanced-structural-subscriptions
+         condition="request/features/malone.advanced-structural-subscriptions.enabled">
+        <tr class="menu-link-subscribe_to_bug_mail invisible-link"
+            tal:define="subscribe_link menu/subscribe_to_bug_mail|nothing"
+            tal:condition="python: subscribe_link and subscribe_link.enabled">
+          <td class="bugs-count" style="padding-top: 3px">
+            <a tal:attributes="href subscribe_link/url">
+              <img tal:attributes="src subscribe_link/icon_url" />
+            </a>
+          </td>
+          <td class="bugs-link">
+            <a class="js-action"
+               tal:attributes="href subscribe_link/url"
+               tal:content="subscribe_link/escapedtext" />
+          </td>
+        </tr>
+      </tal:advanced-structural-subscriptions>
+
+      <tal:not-advanced-structural-subscriptions
+         condition="not: request/features/malone.advanced-structural-subscriptions.enabled">
+        <tr class="menu-link-subscribe"
+            tal:define="subscribe_link menu/subscribe|nothing"
+            tal:condition="python: subscribe_link and subscribe_link.enabled">
+          <td class="bugs-count" style="padding-top: 3px">
+            <a tal:attributes="href subscribe_link/url">
+              <img tal:attributes="src subscribe_link/icon_url" />
+            </a>
+          </td>
+          <td class="bugs-link">
+            <a tal:attributes="href subscribe_link/url"
+               tal:content="subscribe_link/escapedtext" />
+          </td>
+        </tr>
+      </tal:not-advanced-structural-subscriptions>
+
       <tr tal:define="review_nominations_link context/menu:bugs/nominations|nothing"
-          tal:condition="review_nominations_link">
-        <td class="bugs-count" style="padding-top: 1em">
+          tal:condition="review_nominations_link"
+          style="padding-top: 1em">
+        <td class="bugs-count">
           <a tal:attributes="href review_nominations_link/url">
             <img tal:attributes="src review_nominations_link/icon_url" />
           </a>

=== modified file 'lib/lp/bugs/templates/bugtarget-subscription-list.pt'
--- lib/lp/bugs/templates/bugtarget-subscription-list.pt	2011-03-25 21:00:51 +0000
+++ lib/lp/bugs/templates/bugtarget-subscription-list.pt	2011-03-29 15:31:14 +0000
@@ -12,7 +12,9 @@
 
 <head>
   <tal:head-epilogue metal:fill-slot="head_epilogue">
-    <script type="text/javascript">
+    <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
       LPS.use('lp.registry.structural_subscription', function(Y) {
           module = Y.lp.registry.structural_subscription;
           Y.on('domready', function() {

=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2011-03-04 09:55:17 +0000
+++ lib/lp/registry/browser/distribution.py	2011-03-29 15:31:14 +0000
@@ -83,6 +83,8 @@
     )
 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
 from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
+    StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
 from lp.registry.browser import RegistryEditFormView
@@ -104,6 +106,7 @@
     MirrorSpeed,
     )
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services import features
 from lp.services.geoip.helpers import (
     ipaddress_from_request,
     request_country,
@@ -265,8 +268,8 @@
         return Link('+unofficialmirrors', text, enabled=enabled, icon='info')
 
 
-class DistributionLinksMixin:
-    """A mixing to provide common links to menus."""
+class DistributionLinksMixin(StructuralSubscriptionMenuMixin):
+    """A mixin to provide common links to menus."""
 
     @enabled_with_permission('launchpad.Edit')
     def edit(self):
@@ -278,7 +281,15 @@
     """A menu of context actions."""
     usedfor = IDistribution
     facet = 'overview'
-    links = ['edit']
+
+    @cachedproperty
+    def links(self):
+        links = ['edit']
+        use_advanced_features = features.getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        return links
 
 
 class DistributionOverviewMenu(ApplicationMenu, DistributionLinksMixin):
@@ -448,13 +459,22 @@
 
     usedfor = IDistribution
     facet = 'bugs'
-    links = (
-        'bugsupervisor',
-        'securitycontact',
-        'cve',
-        'filebug',
-        'subscribe',
-        )
+
+    @property
+    def links(self):
+        links = [
+            'bugsupervisor',
+            'securitycontact',
+            'cve',
+            'filebug',
+            ]
+        use_advanced_features = features.getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
 
 class DistributionSpecificationsMenu(NavigationMenu,
@@ -594,6 +614,11 @@
 class DistributionView(HasAnnouncementsView, FeedsMixin):
     """Default Distribution view class."""
 
+    def initialize(self):
+        super(DistributionView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
+
     def linkedMilestonesForSeries(self, series):
         """Return a string of linkified milestones in the series."""
         # Listify to remove repeated queries.

=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
--- lib/lp/registry/browser/distributionsourcepackage.py	2011-03-23 16:28:51 +0000
+++ lib/lp/registry/browser/distributionsourcepackage.py	2011-03-29 15:31:14 +0000
@@ -61,6 +61,8 @@
 from lp.app.interfaces.launchpad import IServiceUsage
 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
 from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
+    StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
 from lp.bugs.interfaces.bug import IBugSet
@@ -70,6 +72,7 @@
     )
 from lp.registry.interfaces.pocket import pocketsuffix
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services.features import getFeatureFlag
 from lp.services.propertycache import cachedproperty
 from lp.soyuz.browser.sourcepackagerelease import (
     extract_bug_numbers,
@@ -123,9 +126,6 @@
 
 class DistributionSourcePackageLinksMixin:
 
-    def subscribe(self):
-        return Link('+subscribe', 'Subscribe to bug mail', icon='edit')
-
     def publishinghistory(self):
         return Link('+publishinghistory', 'Show publishing history')
 
@@ -152,17 +152,27 @@
 
     usedfor = IDistributionSourcePackage
     facet = 'overview'
-    links = [
-        'subscribe', 'publishinghistory', 'edit', 'new_bugs',
-        'open_questions']
+    links = ['new_bugs', 'open_questions']
 
 
 class DistributionSourcePackageBugsMenu(
-    PillarBugsMenu, DistributionSourcePackageLinksMixin):
+    PillarBugsMenu,
+    StructuralSubscriptionMenuMixin,
+    DistributionSourcePackageLinksMixin):
 
     usedfor = IDistributionSourcePackage
     facet = 'bugs'
-    links = ['filebug', 'subscribe']
+
+    @cachedproperty
+    def links(self):
+        links = ['filebug']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
 
 class DistributionSourcePackageNavigation(Navigation,
@@ -217,12 +227,26 @@
 
 
 class DistributionSourcePackageActionMenu(
-    NavigationMenu, DistributionSourcePackageLinksMixin):
+    NavigationMenu,
+    StructuralSubscriptionMenuMixin,
+    DistributionSourcePackageLinksMixin):
     """Action menu for distro source packages."""
     usedfor = IDistributionSourcePackageActionMenu
     facet = 'overview'
     title = 'Actions'
-    links = ('publishing_history', 'change_log', 'subscribe', 'edit')
+    links = []
+
+    @cachedproperty
+    def links(self):
+        links = ['publishing_history', 'change_log']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        links.append('edit')
+        return links
 
     def publishing_history(self):
         text = 'View full publishing history'
@@ -295,6 +319,11 @@
     """View class for DistributionSourcePackage."""
     implements(IDistributionSourcePackageActionMenu)
 
+    def initialize(self):
+        super(DistributionSourcePackageView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
+
     @property
     def label(self):
         return self.context.title

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-03-24 14:13:45 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-03-29 15:31:14 +0000
@@ -75,6 +75,7 @@
     )
 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
 from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
     StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
@@ -187,9 +188,28 @@
 
     usedfor = IDistroSeries
     facet = 'overview'
-    links = ['edit', 'reassign', 'driver', 'answers',
-             'packaging', 'needs_packaging', 'builds', 'queue',
-             'add_port', 'create_milestone', 'subscribe', 'admin']
+
+    @property
+    def links(self):
+        links = ['edit',
+                 'reassign',
+                 'driver',
+                 'answers',
+                 'packaging',
+                 'needs_packaging',
+                 'builds',
+                 'queue',
+                 'add_port',
+                 'create_milestone',
+                 ]
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        links.append('admin')
+        return links
 
     @enabled_with_permission('launchpad.Admin')
     def edit(self):
@@ -253,11 +273,19 @@
 
     usedfor = IDistroSeries
     facet = 'bugs'
-    links = (
-        'cve',
-        'nominations',
-        'subscribe',
-        )
+
+    @property
+    def links(self):
+        links = ['cve',
+                 'nominations',
+                 ]
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
     def cve(self):
         return Link('+cve', 'CVE reports', icon='cve')
@@ -328,12 +356,15 @@
             self.context.datereleased = UTC_NOW
 
 
-class DistroSeriesView(MilestoneOverlayMixin):
+class DistroSeriesView(LaunchpadView, MilestoneOverlayMixin):
 
     def initialize(self):
+        super(DistroSeriesView, self).initialize()
         self.displayname = '%s %s' % (
             self.context.distribution.displayname,
             self.context.version)
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
 
     @property
     def page_title(self):

=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py	2011-03-04 00:55:49 +0000
+++ lib/lp/registry/browser/milestone.py	2011-03-29 15:31:14 +0000
@@ -73,6 +73,7 @@
     )
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import IProduct
+from lp.services.features import getFeatureFlag
 from lp.services.propertycache import cachedproperty
 
 
@@ -140,14 +141,35 @@
 class MilestoneContextMenu(ContextMenu, MilestoneLinkMixin):
     """The menu for this milestone."""
     usedfor = IMilestone
-    links = ['edit', 'subscribe', 'create_release']
+
+    @cachedproperty
+    def links(self):
+        links = ['edit']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        links.append('create_release')
+        return links
 
 
 class MilestoneOverviewNavigationMenu(NavigationMenu, MilestoneLinkMixin):
     """Overview navigation menu for `IMilestone` objects."""
     usedfor = IMilestone
     facet = 'overview'
-    links = ('edit', 'delete', 'subscribe')
+
+    @cachedproperty
+    def links(self):
+        links = ['edit', 'delete']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
 
 class MilestoneOverviewMenu(ApplicationMenu, MilestoneLinkMixin):

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2011-03-23 15:55:44 +0000
+++ lib/lp/registry/browser/product.py	2011-03-29 15:31:14 +0000
@@ -153,11 +153,7 @@
     BugTargetTraversalMixin,
     get_buglisting_search_filter_url,
     )
-from lp.bugs.interfaces.bugtask import (
-    RESOLVED_BUGTASK_STATUSES,
-    BugTaskImportance,
-    BugTaskStatus,
-    )
+from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
 from lp.code.browser.branchref import BranchRef
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.registry.browser import BaseRdfView
@@ -173,8 +169,7 @@
     )
 from lp.registry.browser.productseries import get_series_branch_error
 from lp.bugs.browser.structuralsubscription import (
-    expose_enum_to_js,
-    expose_user_administered_teams_to_js,
+    expose_structural_subscription_data_to_js,
     StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
@@ -193,7 +188,7 @@
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
-from lp.services import features
+from lp.services.features import getFeatureFlag
 from lp.services.fields import (
     PillarAliases,
     PublicPersonChoice,
@@ -585,22 +580,17 @@
     facet = 'overview'
     title = 'Actions'
 
-    @property
+    @cachedproperty
     def links(self):
         links = ['edit', 'review_license', 'administer']
-        use_advanced_features = features.getFeatureFlag(
-            'advanced-structural-subscriptions.enabled')
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
         if use_advanced_features:
             links.append('subscribe_to_bug_mail')
         else:
             links.append('subscribe')
         return links
 
-    @enabled_with_permission('launchpad.AnyPerson')
-    def subscribe_to_bug_mail(self):
-        text = 'Subscribe to bug mail'
-        return Link('#', text, icon='add', hidden=True)
-
 
 class ProductOverviewMenu(ApplicationMenu, ProductEditLinksMixin,
                           HasRecipesMenuMixin):
@@ -694,16 +684,26 @@
 
     usedfor = IProduct
     facet = 'bugs'
-    links = (
-        'filebug',
-        'bugsupervisor',
-        'securitycontact',
-        'cve',
-        'subscribe',
-        'configure_bugtracker',
-        )
     configurable_bugtracker = True
 
+    @cachedproperty
+    def links(self):
+        links = [
+            'filebug',
+            'bugsupervisor',
+            'securitycontact',
+            'cve',
+            ]
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        links.append('configure_bugtracker')
+
+        return links
+
 
 class ProductSpecificationsMenu(NavigationMenu, ProductEditLinksMixin,
                                 HasSpecificationsMenuMixin):
@@ -1009,6 +1009,7 @@
         self.form = request.form_ng
 
     def initialize(self):
+        super(ProductView, self).initialize()
         self.status_message = None
         product = self.context
         title_field = IProduct['title']
@@ -1028,9 +1029,8 @@
         self.show_programming_languages = bool(
             self.context.programminglang or
             check_permission('launchpad.Edit', self.context))
-        expose_user_administered_teams_to_js(self.request, self.user)
-        expose_enum_to_js(self.request, BugTaskImportance, 'importances')
-        expose_enum_to_js(self.request, BugTaskStatus, 'statuses')
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
 
     @property
     def show_license_status(self):

=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py	2011-02-03 10:35:36 +0000
+++ lib/lp/registry/browser/productseries.py	2011-03-29 15:31:14 +0000
@@ -128,6 +128,7 @@
     PillarView,
     )
 from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
     StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
@@ -137,6 +138,7 @@
     )
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services.features import getFeatureFlag
 from lp.services.fields import URIField
 from lp.services.propertycache import cachedproperty
 from lp.services.worlddata.interfaces.country import ICountry
@@ -279,19 +281,28 @@
     """The overview menu."""
     usedfor = IProductSeries
     facet = 'overview'
-    links = [
-        'configure_bugtracker',
-        'create_milestone',
-        'create_release',
-        'delete',
-        'driver',
-        'edit',
-        'link_branch',
-        'rdf',
-        'set_branch',
-        'subscribe',
-        'ubuntupkg',
-        ]
+
+    @cachedproperty
+    def links(self):
+        links = [
+            'configure_bugtracker',
+            'create_milestone',
+            'create_release',
+            'delete',
+            'driver',
+            'edit',
+            'link_branch',
+            'rdf',
+            'set_branch',
+            ]
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        links.append('ubuntupkg')
+        return links
 
     @enabled_with_permission('launchpad.Edit')
     def configure_bugtracker(self):
@@ -380,11 +391,17 @@
     """The bugs menu."""
     usedfor = IProductSeries
     facet = 'bugs'
-    links = (
-        'new',
-        'nominations',
-        'subscribe',
-        )
+
+    @cachedproperty
+    def links(self):
+        links = ['new', 'nominations']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
     def new(self):
         """Return a link to report a bug in this series."""
@@ -439,6 +456,11 @@
 class ProductSeriesView(LaunchpadView, MilestoneOverlayMixin):
     """A view to show a series with translations."""
 
+    def initialize(self):
+        super(ProductSeriesView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
+
     @property
     def page_title(self):
         """Return the HTML page title."""

=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py	2011-01-21 08:30:55 +0000
+++ lib/lp/registry/browser/project.py	2011-03-29 15:31:14 +0000
@@ -88,6 +88,8 @@
     ProjectAddStepTwo,
     )
 from lp.bugs.browser.structuralsubscription import (
+    expose_structural_subscription_data_to_js,
+    StructuralSubscriptionMenuMixin,
     StructuralSubscriptionTargetTraversalMixin,
     )
 from lp.registry.interfaces.product import IProductSet
@@ -96,6 +98,7 @@
     IProjectGroupSeries,
     IProjectGroupSet,
     )
+from lp.services.features import getFeatureFlag
 from lp.services.fields import (
     PillarAliases,
     PublicPersonChoice,
@@ -270,20 +273,24 @@
     """Marker interface for views that use ProjectActionMenu."""
 
 
-class ProjectActionMenu(ProjectAdminMenuMixin, NavigationMenu):
+class ProjectActionMenu(ProjectAdminMenuMixin,
+                        StructuralSubscriptionMenuMixin,
+                        NavigationMenu):
 
     usedfor = IProjectGroupActionMenu
     facet = 'overview'
     title = 'Action menu'
-    links = ('subscribe', 'edit', 'administer')
 
-    # XXX: salgado, bug=412178, 2009-08-10: This should be shown in the +index
-    # page of the project's bugs facet, but that would require too much work
-    # and I just want to convert this page to 3.0, so I'll leave it here for
-    # now.
-    def subscribe(self):
-        text = 'Subscribe to bug mail'
-        return Link('+subscribe', text, icon='edit')
+    @cachedproperty
+    def links(self):
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links = ['subscribe_to_bug_mail']
+        else:
+            links = ['subscribe']
+        links.extend(['edit', 'administer'])
+        return links
 
     @enabled_with_permission('launchpad.Edit')
     def edit(self):
@@ -323,24 +330,36 @@
         return Link('+addquestion', text, icon='add')
 
 
-class ProjectBugsMenu(ApplicationMenu):
+class ProjectBugsMenu(StructuralSubscriptionMenuMixin,
+                      ApplicationMenu):
 
     usedfor = IProjectGroup
     facet = 'bugs'
-    links = ['new', 'subscribe']
+
+    @cachedproperty
+    def links(self):
+        links = ['new']
+        use_advanced_features = getFeatureFlag(
+            'malone.advanced-structural-subscriptions.enabled')
+        if use_advanced_features:
+            links.append('subscribe_to_bug_mail')
+        else:
+            links.append('subscribe')
+        return links
 
     def new(self):
         text = 'Report a Bug'
         return Link('+filebug', text, icon='add')
 
-    def subscribe(self):
-        text = 'Subscribe to bug mail'
-        return Link('+subscribe', text, icon='edit')
-
 
 class ProjectView(HasAnnouncementsView, FeedsMixin):
     implements(IProjectGroupActionMenu)
 
+    def initialize(self):
+        super(ProjectView, self).initialize()
+        expose_structural_subscription_data_to_js(
+            self.context, self.request, self.user)
+
     @cachedproperty
     def has_many_projects(self):
         """Does the projectgroup have many sub projects.

=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py	2011-03-07 19:53:40 +0000
+++ lib/lp/registry/browser/tests/test_product.py	2011-03-29 15:31:14 +0000
@@ -13,10 +13,14 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.config import config
-from canonical.launchpad.testing.pages import find_tag_by_id
+from canonical.launchpad.testing.pages import (
+    find_tag_by_id,
+    )
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.app.enums import ServiceUsage
-from lp.registry.browser.product import ProductLicenseMixin
+from lp.registry.browser.product import (
+    ProductLicenseMixin,
+    )
 from lp.registry.interfaces.product import (
     License,
     IProductSet,

=== added file 'lib/lp/registry/browser/tests/test_subscription_links.py'
--- lib/lp/registry/browser/tests/test_subscription_links.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_subscription_links.py	2011-03-29 15:31:14 +0000
@@ -0,0 +1,619 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for subscription links."""
+
+__metaclass__ = type
+
+import unittest
+from zope.component import getUtility
+from BeautifulSoup import BeautifulSoup
+
+from canonical.launchpad.webapp.interaction import ANONYMOUS
+from canonical.launchpad.webapp.interfaces import ILaunchBag
+from canonical.launchpad.webapp.publisher import canonical_url
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.launchpad.testing.pages import (
+    first_tag_by_class,
+    )
+from canonical.testing.layers import DatabaseFunctionalLayer
+
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.features import (
+    get_relevant_feature_controller,
+    )
+from lp.services.features.testing import FeatureFixture
+from lp.testing import (
+    celebrity_logged_in,
+    person_logged_in,
+    BrowserTestCase,
+    TestCaseWithFactory,
+    )
+from lp.testing.sampledata import ADMIN_EMAIL
+from lp.testing.views import (
+    create_initialized_view,
+    )
+
+
+class _TestStructSubs(TestCaseWithFactory):
+    """Test structural subscriptions base class.
+
+    The link to structural subscriptions is controlled by the feature flag
+    'malone.advanced-structural-subscriptions.enabled'.  If it is false, the
+    old link leading to +subscribe is shown.  If it is true then the new
+    JavaScript control is used.
+    """
+
+    layer = DatabaseFunctionalLayer
+    feature_flag = 'malone.advanced-structural-subscriptions.enabled'
+
+    def setUp(self):
+        super(_TestStructSubs, self).setUp()
+        self.regular_user = self.factory.makePerson()
+
+    def _create_scenario(self, user, flag):
+        with person_logged_in(user):
+            with FeatureFixture({self.feature_flag: flag}):
+                view = self.create_view(user)
+                self.contents = view.render()
+                old_link = first_tag_by_class(
+                    self.contents, 'menu-link-subscribe')
+                new_link = first_tag_by_class(
+                    self.contents, 'menu-link-subscribe_to_bug_mail')
+                return old_link, new_link
+
+    def create_view(self, user):
+        request = LaunchpadTestRequest(
+            PATH_INFO='/', HTTP_COOKIE='', QUERY_STRING='')
+        request.features = get_relevant_feature_controller()
+        return create_initialized_view(
+            self.target, self.view, principal=user,
+            rootsite=self.rootsite,
+            request=request, current_request=False)
+
+    def test_subscribe_link_feature_flag_off_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.target.owner, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_owner(self):
+        # Test the new subscription link.
+        old_link, new_link = self._create_scenario(
+            self.target.owner, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_anonymous(self):
+        old_link, new_link = self._create_scenario(
+            ANONYMOUS, None)
+        # The old subscribe link is actually shown to anonymous users but the
+        # behavior has changed with the new link.
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_anonymous(self):
+        old_link, new_link = self._create_scenario(
+            ANONYMOUS, 'on')
+        # The subscribe link is not shown to anonymous.
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+
+class TestProductViewStructSubs(_TestStructSubs):
+    """Test structural subscriptions on the product view."""
+
+    rootsite = None
+    view = '+index'
+
+    def setUp(self):
+        super(TestProductViewStructSubs, self).setUp()
+        self.target = self.factory.makeProduct(official_malone=True)
+
+
+class TestProductBugsStructSubs(TestProductViewStructSubs):
+    """Test structural subscriptions on the product bugs view."""
+
+    rootsite = 'bugs'
+    view = '+bugs-index'
+
+
+class TestProjectGroupViewStructSubs(_TestStructSubs):
+    """Test structural subscriptions on the project group view."""
+
+    rootsite = None
+    view = '+index'
+
+    def setUp(self):
+        super(TestProjectGroupViewStructSubs, self).setUp()
+        self.target = self.factory.makeProject()
+        self.factory.makeProduct(
+            project=self.target, official_malone=True)
+
+
+class TestProjectGroupBugsStructSubs(TestProjectGroupViewStructSubs):
+    """Test structural subscriptions on the project group bugs view."""
+
+    rootsite = 'bugs'
+    view = '+bugs'
+
+
+class TestProductSeriesViewStructSubs(_TestStructSubs):
+    """Test structural subscriptions on the product series view."""
+
+    rootsite = None
+    view = '+index'
+
+    def setUp(self):
+        super(TestProductSeriesViewStructSubs, self).setUp()
+        self.target = self.factory.makeProductSeries()
+
+
+class TestProductSeriesBugsStructSubs(TestProductSeriesViewStructSubs):
+    """Test structural subscriptions on the product series bugs view."""
+
+    rootsite = 'bugs'
+    view = '+bugs-index'
+
+    def setUp(self):
+        super(TestProductSeriesBugsStructSubs, self).setUp()
+        with person_logged_in(self.target.product.owner):
+            self.target.product.official_malone = True
+
+
+class TestDistributionSourcePackageViewStructSubs(_TestStructSubs):
+    """Test structural subscriptions on the distro src pkg view."""
+
+    rootsite = None
+    view = '+index'
+
+    def setUp(self):
+        super(TestDistributionSourcePackageViewStructSubs, self).setUp()
+        distro = self.factory.makeDistribution()
+        with person_logged_in(distro.owner):
+            distro.official_malone = True
+        self.target = self.factory.makeDistributionSourcePackage(
+            distribution=distro)
+        self.regular_user = self.factory.makePerson()
+
+    # DistributionSourcePackages do not have owners.
+    test_subscribe_link_feature_flag_off_owner = None
+    test_subscribe_link_feature_flag_on_owner = None
+
+
+class TestDistributionSourcePackageBugsStructSubs(
+    TestDistributionSourcePackageViewStructSubs):
+    """Test structural subscriptions on the distro src pkg bugs view."""
+
+    rootsite = 'bugs'
+    view = '+bugs'
+
+
+class TestDistroViewStructSubs(BrowserTestCase):
+    """Test structural subscriptions on the distribution view.
+
+    Distributions are special.  They are IStructuralSubscriptionTargets but
+    have complicated rules to ensure Ubuntu users don't subscribe and become
+    overwhelmed with email.  If a distro does not have a bug supervisor set,
+    then anyone can create a structural subscription for themselves.  If the
+    bug supervisor is set, then only people in the bug supervisor team can
+    subscribe themselves.  Admins can subscribe anyone.
+    """
+
+    layer = DatabaseFunctionalLayer
+    feature_flag = 'malone.advanced-structural-subscriptions.enabled'
+    rootsite = None
+    view = '+index'
+
+    def setUp(self):
+        super(TestDistroViewStructSubs, self).setUp()
+        self.target = self.factory.makeDistribution()
+        with person_logged_in(self.target.owner):
+            self.target.official_malone = True
+        self.regular_user = self.factory.makePerson()
+
+    def _create_scenario(self, user, flag):
+        with person_logged_in(user):
+            with FeatureFixture({self.feature_flag: flag}):
+                logged_in_user = getUtility(ILaunchBag).user
+                browser = self.getViewBrowser(
+                    self.target, view_name=self.view,
+                    rootsite=self.rootsite,
+                    user=logged_in_user)
+                self.contents = browser.contents
+                soup = BeautifulSoup(self.contents)
+                href = canonical_url(
+                    self.target, rootsite=self.rootsite,
+                    view_name='+subscribe')
+                old_link = soup.find('a', href=href)
+                new_link = first_tag_by_class(
+                    self.contents, 'menu-link-subscribe_to_bug_mail')
+                return old_link, new_link
+
+    def test_subscribe_link_feature_flag_off_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.target.owner, None)
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.target.owner, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            supervisor = self.factory.makePerson()
+            self.target.setBugSupervisor(
+                supervisor, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    # Can't do ANONYMOUS testing with BrowserTestCase as it creates a new,
+    # valid user when it encounters ANONYMOUS.
+
+    ## def test_subscribe_link_feature_flag_off_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, None)
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    ## def test_subscribe_link_feature_flag_on_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, 'on')
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.target.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.target.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_admin(self):
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, None)
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_admin(self):
+        from lp.testing.sampledata import ADMIN_EMAIL
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+
+class TestDistroBugsStructSubs(TestDistroViewStructSubs):
+    """Test structural subscriptions on the distro bugs view."""
+
+    rootsite = 'bugs'
+    view = '+bugs-index'
+
+    def test_subscribe_link_feature_flag_off_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.target.owner, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.target.owner, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            supervisor = self.factory.makePerson()
+            self.target.setBugSupervisor(
+                supervisor, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    # Can't do ANONYMOUS testing with BrowserTestCase as it creates a new,
+    # valid user when it encounters ANONYMOUS.
+
+    ## def test_subscribe_link_feature_flag_off_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, None)
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    ## def test_subscribe_link_feature_flag_on_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, 'on')
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.target.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.target.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_admin(self):
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_admin(self):
+        from lp.testing.sampledata import ADMIN_EMAIL
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+
+class TestDistroMilestoneViewStructSubs(TestDistroViewStructSubs):
+    """Test structural subscriptions on the distro milestones."""
+
+    def setUp(self):
+        super(TestDistroMilestoneViewStructSubs, self).setUp()
+        self.distro = self.target
+        self.target = self.factory.makeMilestone(distribution=self.distro)
+
+    def test_subscribe_link_feature_flag_off_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.distro.owner, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.distro.owner, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_no_bug_super(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_user_with_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            supervisor = self.factory.makePerson()
+            self.distro.setBugSupervisor(
+                supervisor, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    # Can't do ANONYMOUS testing with BrowserTestCase as it creates a new,
+    # valid user when it encounters ANONYMOUS.
+
+    ## def test_subscribe_link_feature_flag_off_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, None)
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    ## def test_subscribe_link_feature_flag_on_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, 'on')
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.distro.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_bug_super(self):
+        with celebrity_logged_in('admin'):
+            admin = getUtility(ILaunchBag).user
+            self.distro.setBugSupervisor(
+                self.regular_user, admin)
+        old_link, new_link = self._create_scenario(
+            self.regular_user, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_admin(self):
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_admin(self):
+        from lp.testing.sampledata import ADMIN_EMAIL
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+
+class TestProductMilestoneViewStructSubs(TestDistroViewStructSubs):
+    """Test structural subscriptions on the product milestones."""
+
+    def setUp(self):
+        super(TestProductMilestoneViewStructSubs, self).setUp()
+        self.product = self.factory.makeProduct()
+        with person_logged_in(self.product.owner):
+            self.product.official_malone = True
+        self.regular_user = self.factory.makePerson()
+        self.target = self.factory.makeMilestone(product=self.product)
+
+    def test_subscribe_link_feature_flag_off_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.product.owner, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_owner(self):
+        old_link, new_link = self._create_scenario(
+            self.product.owner, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_user(self):
+        old_link, new_link = self._create_scenario(
+            self.regular_user, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    # There are no special bug supervisor rules for products.
+    test_subscribe_link_feature_flag_on_user_no_bug_super = None
+    test_subscribe_link_feature_flag_on_user_with_bug_super = None
+    test_subscribe_link_feature_flag_off_bug_super = None
+    test_subscribe_link_feature_flag_on_bug_super = None
+
+    # Can't do ANONYMOUS testing with BrowserTestCase as it creates a new,
+    # valid user when it encounters ANONYMOUS.
+
+    ## def test_subscribe_link_feature_flag_off_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, None)
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    ## def test_subscribe_link_feature_flag_on_anonymous(self):
+    ##     old_link, new_link = self._create_scenario(
+    ##         ANONYMOUS, 'on')
+    ##     self.assertEqual(None, old_link, self.contents)
+    ##     self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_off_admin(self):
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, None)
+        self.assertNotEqual(None, old_link, self.contents)
+        self.assertEqual(None, new_link, self.contents)
+
+    def test_subscribe_link_feature_flag_on_admin(self):
+        from lp.testing.sampledata import ADMIN_EMAIL
+        admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+        old_link, new_link = self._create_scenario(
+            admin, 'on')
+        self.assertEqual(None, old_link, self.contents)
+        self.assertNotEqual(None, new_link, self.contents)
+
+
+class TestProductSeriesMilestoneViewStructSubs(
+    TestProductMilestoneViewStructSubs):
+    """Test structural subscriptions on the product series milestones."""
+
+    def setUp(self):
+        super(TestProductSeriesMilestoneViewStructSubs, self).setUp()
+        self.productseries = self.factory.makeProductSeries()
+        with person_logged_in(self.productseries.product.owner):
+            self.productseries.product.official_malone = True
+        self.regular_user = self.factory.makePerson()
+        self.target = self.factory.makeMilestone(
+            productseries=self.productseries)
+
+
+def test_suite():
+    """Return the `IStructuralSubscriptionTarget` TestSuite."""
+
+    # Manually construct the test suite to avoid having tests from the base
+    # class _TestStructSubs run.
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestProductViewStructSubs))
+    suite.addTest(unittest.makeSuite(TestProductBugsStructSubs))
+    suite.addTest(unittest.makeSuite(TestProductSeriesViewStructSubs))
+    suite.addTest(unittest.makeSuite(TestProductSeriesBugsStructSubs))
+    suite.addTest(unittest.makeSuite(TestProjectGroupViewStructSubs))
+    suite.addTest(unittest.makeSuite(TestProjectGroupBugsStructSubs))
+    suite.addTest(unittest.makeSuite(
+        TestDistributionSourcePackageViewStructSubs))
+    suite.addTest(unittest.makeSuite(
+        TestDistributionSourcePackageBugsStructSubs))
+    suite.addTest(unittest.makeSuite(TestDistroViewStructSubs))
+    suite.addTest(unittest.makeSuite(TestDistroBugsStructSubs))
+    suite.addTest(unittest.makeSuite(TestDistroMilestoneViewStructSubs))
+    suite.addTest(unittest.makeSuite(TestProductMilestoneViewStructSubs))
+    suite.addTest(unittest.makeSuite(
+        TestProductSeriesMilestoneViewStructSubs))
+    return suite

=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js	2011-03-28 19:24:42 +0000
+++ lib/lp/registry/javascript/structural-subscription.js	2011-03-29 15:31:14 +0000
@@ -80,7 +80,7 @@
         tags: [],
         find_all_tags: false,
         importances: [],
-        statuses: [],
+        statuses: []
     };
 
     // Set the notification level.

=== modified file 'lib/lp/registry/templates/distribution-index.pt'
--- lib/lp/registry/templates/distribution-index.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/distribution-index.pt	2011-03-29 15:31:14 +0000
@@ -5,6 +5,22 @@
   xmlns:i18n="http://xml.zope.org/namespaces/i18n";
   metal:use-macro="view/macro:page/main_side"
   i18n:domain="launchpad">
+
+  <head>
+    <tal:head-epilogue metal:fill-slot="head_epilogue">
+      <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
+          LPS.use('lp.registry.structural_subscription', function(Y) {
+              module = Y.lp.registry.structural_subscription;
+              Y.on('domready', function() {
+                module.setup({content_box: "#structural-subscription-content-box"});
+              });
+          });
+      </script>
+    </tal:head-epilogue>
+  </head>
+
   <body>
     <tal:heading metal:fill-slot="heading">
       <h1 tal:content="context/title">project title</h1>
@@ -69,6 +85,9 @@
 
           <div tal:replace="structure context/@@+portlet-coming-sprints" />
         </div>
+        <div class="yui-u">
+          <div id="structural-subscription-content-box"></div>
+        </div>
       </div>
     </tal:main>
 

=== modified file 'lib/lp/registry/templates/distributionsourcepackage-index.pt'
--- lib/lp/registry/templates/distributionsourcepackage-index.pt	2011-03-23 16:28:51 +0000
+++ lib/lp/registry/templates/distributionsourcepackage-index.pt	2011-03-29 15:31:14 +0000
@@ -16,6 +16,16 @@
               tal:attributes="src string:${lp_js}/soyuz/base.js"></script>
     </tal:archive_js>
   </tal:devmode>
+  <script type="text/javascript"
+      tal:condition="
+        request/features/malone.advanced-structural-subscriptions.enabled">
+    LPS.use('lp.registry.structural_subscription', function(Y) {
+        module = Y.lp.registry.structural_subscription;
+        Y.on('domready', function() {
+          module.setup({content_box: "#structural-subscription-content-box"});
+        });
+    });
+  </script>
 </metal:block>
 
 <tal:side metal:fill-slot="side">
@@ -28,6 +38,9 @@
 </tal:side>
 
 <tal:main metal:fill-slot="main">
+  <div class="yui-u">
+    <div id="structural-subscription-content-box"></div>
+  </div>
   <div class="top-portlet" id="bugs-and-questions-summary"
        tal:define="newbugs context/new_bugtasks/count;
                    open_questions view/open_questions/count">
@@ -169,7 +182,7 @@
       <tr tal:attributes="id string:pub${pubid}" style="display: none">
         <td colspan="3">
           <div class="package-details"
-               tal:attributes="id string:pub${pubid}-container" />
+               tal:attributes="id string:pub${pubid}-container"></div>
         </td>
       </tr>
 

=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
--- lib/lp/registry/templates/distroseries-index.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/distroseries-index.pt	2011-03-29 15:31:14 +0000
@@ -5,6 +5,7 @@
   xmlns:i18n="http://xml.zope.org/namespaces/i18n";
   metal:use-macro="view/macro:page/main_side"
   i18n:domain="launchpad">
+
   <body>
     <metal:block fill-slot="head_epilogue">
       <metal:yui-dependencies
@@ -12,6 +13,16 @@
       <script id="milestone-script" type="text/javascript"
         tal:condition="context/menu:overview/create_milestone/enabled"
         tal:content="view/register_milestone_script"></script>
+      <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
+          LPS.use('lp.registry.structural_subscription', function(Y) {
+              module = Y.lp.registry.structural_subscription;
+              Y.on('domready', function() {
+                module.setup({content_box: "#structural-subscription-content-box"});
+              });
+          });
+      </script>
     </metal:block>
 
     <tal:heading metal:fill-slot="heading">
@@ -108,11 +119,15 @@
             </ul>
           </div>
         </div>
+        <div class="yui-u">
+          <div id="structural-subscription-content-box"></div>
+        </div>
       </div>
     </div>
 
     <tal:side metal:fill-slot="side"
-      define="overview_menu context/menu:overview">
+      define="overview_menu context/menu:overview"
+      condition="overview_menu/subscribe_to_bug_mail/enabled|nothing">
       <div id="global-actions" class="portlet">
         <ul>
           <li tal:condition="overview_menu/edit/enabled">
@@ -124,9 +139,12 @@
           <li tal:condition="overview_menu/reassign/enabled">
             <a tal:replace="structure overview_menu/reassign/fmt:link" />
           </li>
-          <li>
+          <li tal:condition="overview_menu/subscribe/enabled">
             <a tal:replace="structure overview_menu/subscribe/fmt:link" />
           </li>
+          <li tal:condition="overview_menu/subscribe_to_bug_mail/enabled">
+            <a tal:replace="structure overview_menu/subscribe_to_bug_mail/fmt:link" />
+          </li>
         </ul>
       </div>
 

=== modified file 'lib/lp/registry/templates/milestone-index.pt'
--- lib/lp/registry/templates/milestone-index.pt	2011-03-04 00:08:20 +0000
+++ lib/lp/registry/templates/milestone-index.pt	2011-03-29 15:31:14 +0000
@@ -6,14 +6,24 @@
   metal:use-macro="view/macro:page/main_side"
   i18n:domain="launchpad">
 
-  <tal:css metal:fill-slot="head_epilogue"
+  <tal:head-epilogue metal:fill-slot="head_epilogue"
     condition="view/is_project_milestone">
     <style id="hide-side-portlets" type="text/css">
         .side {
             background: #fff;
             }
     </style>
-  </tal:css>
+    <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
+        LPS.use('lp.registry.structural_subscription', function(Y) {
+            module = Y.lp.registry.structural_subscription;
+            Y.on('domready', function() {
+              module.setup({content_box: "#structural-subscription-content-box"});
+            });
+        });
+    </script>
+  </tal:head-epilogue>
 
   <body>
     <tal:heading metal:fill-slot="heading">
@@ -308,6 +318,9 @@
           </li>
         </ul>
       </div>
+      <div class="yui-u">
+        <div id="structural-subscription-content-box"></div>
+      </div>
     </div>
 
     <tal:side metal:fill-slot="side">

=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt	2011-03-23 15:55:44 +0000
+++ lib/lp/registry/templates/product-index.pt	2011-03-29 15:31:14 +0000
@@ -34,7 +34,7 @@
 
     <script type="text/javascript"
         tal:condition="
-          request/features/advanced-structural-subscriptions.enabled">
+          request/features/malone.advanced-structural-subscriptions.enabled">
         LPS.use('lp.registry.structural_subscription', function(Y) {
             module = Y.lp.registry.structural_subscription;
             Y.on('domready', function() {

=== modified file 'lib/lp/registry/templates/productseries-index.pt'
--- lib/lp/registry/templates/productseries-index.pt	2011-03-23 16:28:51 +0000
+++ lib/lp/registry/templates/productseries-index.pt	2011-03-29 15:31:14 +0000
@@ -14,6 +14,16 @@
     <script id="milestone-script" type="text/javascript"
       tal:condition="context/menu:overview/create_milestone/enabled"
       tal:content="view/register_milestone_script"></script>
+    <script type="text/javascript"
+        tal:condition="
+          request/features/malone.advanced-structural-subscriptions.enabled">
+        LPS.use('lp.registry.structural_subscription', function(Y) {
+            module = Y.lp.registry.structural_subscription;
+            Y.on('domready', function() {
+              module.setup({content_box: "#structural-subscription-content-box"});
+            });
+        });
+    </script>
   </metal:block>
 
     <tal:heading metal:fill-slot="heading">
@@ -178,12 +188,17 @@
             </ul>
           </div>
         </div>
+        <div class="yui-u">
+          <div id="structural-subscription-content-box"></div>
+        </div>
       </div>
     </div>
 
     <tal:side metal:fill-slot="side"
-      define="overview_menu context/menu:overview">
-      <div id="global-actions" class="portlet">
+      define="overview_menu context/menu:overview;
+              feature_flag request/features/malone.advanced-structural-subscriptions.enabled">
+      <div id="global-actions" class="portlet"
+        tal:condition="overview_menu/subscribe_to_bug_mail/enabled|overview_menu/subscribe/enabled">
         <ul>
           <li tal:condition="overview_menu/edit/enabled">
             <a tal:replace="structure overview_menu/edit/fmt:link" />
@@ -191,13 +206,21 @@
           <li tal:condition="overview_menu/delete/enabled">
             <a tal:replace="structure overview_menu/delete/fmt:link" />
           </li>
-          <li>
-            <a tal:replace="structure overview_menu/subscribe/fmt:link" />
-          </li>
+          <tal:advanced-structural-subscriptions
+             condition="feature_flag">
+            <li tal:condition="overview_menu/subscribe_to_bug_mail/enabled|nothing">
+              <a tal:replace="structure overview_menu/subscribe_to_bug_mail/fmt:link" />
+            </li>
+          </tal:advanced-structural-subscriptions>
+          <tal:not-advanced-structural-subscriptions
+             condition="not: feature_flag">
+            <li tal:condition="overview_menu/subscribe/enabled|nothing">
+              <a tal:replace="structure overview_menu/subscribe/fmt:link" />
+            </li>
+          </tal:not-advanced-structural-subscriptions>
         </ul>
       </div>
 
-
       <div id="downloads" class="top-portlet downloads"
         tal:define="release view/latest_release_with_download_files">
         <h2>Downloads</h2>

=== modified file 'lib/lp/registry/templates/project-index.pt'
--- lib/lp/registry/templates/project-index.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/project-index.pt	2011-03-29 15:31:14 +0000
@@ -6,6 +6,22 @@
   metal:use-macro="view/macro:page/main_side"
   i18n:domain="launchpad"
 >
+
+  <head>
+    <tal:head-epilogue metal:fill-slot="head_epilogue">
+      <script type="text/javascript"
+          tal:condition="
+            request/features/malone.advanced-structural-subscriptions.enabled">
+          LPS.use('lp.registry.structural_subscription', function(Y) {
+              module = Y.lp.registry.structural_subscription;
+              Y.on('domready', function() {
+                module.setup({content_box: "#structural-subscription-content-box"});
+              });
+          });
+      </script>
+    </tal:head-epilogue>
+  </head>
+
   <body>
     <tal:registering metal:fill-slot="registering">
       Registered
@@ -127,6 +143,9 @@
           <tal:sprints content="structure context/@@+portlet-coming-sprints" />
         </tal:has-few-project>
       </div>
+      <div class="yui-u">
+        <div id="structural-subscription-content-box"></div>
+      </div>
     </div>
     </tal:main>
 

=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py	2011-03-28 05:33:33 +0000
+++ lib/lp/services/features/flags.py	2011-03-29 15:31:14 +0000
@@ -53,6 +53,10 @@
      'boolean',
      'Enables advanced bug subscription features.',
      ''),
+    ('malone.advanced-structural-subscriptions.enabled',
+     'boolean',
+     'Enables advanced structural subscriptions',
+     ''),
     ('malone.disable_targetnamesearch',
      'boolean',
      'If true, disables consultation of target names during bug text search.',