← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~gmb/launchpad/add-update-subscription-workflow-bug-664571 into lp:launchpad/devel

 

Graham Binns has proposed merging lp:~gmb/launchpad/add-update-subscription-workflow-bug-664571 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
  #664571 Users should have the option to alter a subscription's BugNotificationLevel
  https://bugs.launchpad.net/bugs/664571


This branch fixes bug 664571 by adding a workflow to the bug +subscribe
view that allows users to update the BugNotificationLevel of an existing
bug subscription.

Changes made:

== lib/canonical/launchpad/webapp/servers.py ==

 - I've added a clearSecurityPolicyCache() stub to LaunchpadTestRequest
   so that the +subscribe view doesn't choke on its non-existance.

== lib/lp/bugs/browser/bugsubscription.py ==

 - I've added a workflow that allows a subscriber to update their
   current bug subscription to a new BugNotificationLevel.
 - I've done a little refactoring to make the class a bit tidier.

== lib/lp/bugs/browser/tests/test_bugsubscription_views.py ==

 - I've added a couple of tests to cover the update-subscription
   workflow.
 - I've added a unit test to cover unsubscribing from a bug using the
   +subscribe form (just in case we do deprecate pagetests after all).

== lib/lp/bugs/configure.zcml ==

 - I've updated this to reflect the changes to IBug (below).

== lib/lp/bugs/interfaces/bug.py ==

 - I've added a method declaration for getSubscriptionForPerson() to
   IBug. This will allow us to get the BugSubscription object for a
   given person on a bug so that we can update it.

== lib/lp/bugs/model/bug.py ==

 - I've added the implementation for getSubscriptionForPerson() to Bug.

-- 
https://code.launchpad.net/~gmb/launchpad/add-update-subscription-workflow-bug-664571/+merge/39966
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~gmb/launchpad/add-update-subscription-workflow-bug-664571 into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py	2010-10-25 13:16:10 +0000
+++ lib/canonical/launchpad/webapp/servers.py	2010-11-03 14:03:50 +0000
@@ -912,6 +912,10 @@
         """See `IPublicationRequest`."""
         self.principal = principal
 
+    def clearSecurityPolicyCache(self):
+        """See ILaunchpadBrowserApplicationRequest."""
+        return
+
 
 class LaunchpadTestResponse(LaunchpadBrowserResponse):
     """Mock response for use in unit and functional tests.

=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
--- lib/lp/bugs/browser/bugsubscription.py	2010-10-22 16:01:06 +0000
+++ lib/lp/bugs/browser/bugsubscription.py	2010-11-03 14:03:50 +0000
@@ -152,6 +152,10 @@
         super(BugSubscriptionSubscribeSelfView, self).initialize()
 
     @cachedproperty
+    def current_user_subscription(self):
+        return self.context.bug.getSubscriptionForPerson(self.user)
+
+    @cachedproperty
     def _bug_notification_level_field(self):
         """Return a custom form field for bug_notification_level."""
         # We rebuild the items that we show in the field so that the
@@ -169,22 +173,33 @@
             ]
         bug_notification_vocabulary = SimpleVocabulary(
             bug_notification_level_terms)
+
+        if self.current_user_subscription is not None:
+            default_value = (
+                self.current_user_subscription.bug_notification_level)
+        else:
+            default_value = BugNotificationLevel.COMMENTS
+
         bug_notification_level_field = Choice(
             __name__='bug_notification_level', title=_("Tell me when"),
             vocabulary=bug_notification_vocabulary, required=True,
-            default=BugNotificationLevel.COMMENTS)
+            default=default_value)
         return bug_notification_level_field
 
-    def setUpFields(self):
-        """See `LaunchpadFormView`."""
-        super(BugSubscriptionSubscribeSelfView, self).setUpFields()
-        if self.user is None:
-            return
+    @cachedproperty
+    def _update_subscription_term(self):
+        return SimpleTerm(
+            'update-subscription', 'update-subscription',
+            'Update my current subscription')
 
+    @cachedproperty
+    def _subscription_field(self):
         subscription_terms = []
         self_subscribed = False
         for person in self._subscribers_for_current_user:
             if person.id == self.user.id:
+                if self._use_advanced_features:
+                    subscription_terms.append(self._update_subscription_term)
                 subscription_terms.append(
                     SimpleTerm(
                         person, person.name,
@@ -202,22 +217,32 @@
                 SimpleTerm(
                     self.user, self.user.name, 'Subscribe me to this bug'))
         subscription_vocabulary = SimpleVocabulary(subscription_terms)
-        default_subscription_value = subscription_vocabulary.getTermByToken(
-            self.user.name).value
+        if self.user_is_subscribed and self._use_advanced_features:
+            default_subscription_value = self._update_subscription_term.value
+        else:
+            default_subscription_value = (
+                subscription_vocabulary.getTermByToken(self.user.name).value)
         subscription_field = Choice(
             __name__='subscription', title=_("Subscription options"),
             vocabulary=subscription_vocabulary, required=True,
             default=default_subscription_value)
+        return subscription_field
+
+    def setUpFields(self):
+        """See `LaunchpadFormView`."""
+        super(BugSubscriptionSubscribeSelfView, self).setUpFields()
+        if self.user is None:
+            return
 
         if self._use_advanced_features:
             self.form_fields = self.form_fields.omit('bug_notification_level')
-            self.form_fields += formlib.form.Fields(subscription_field)
+            self.form_fields += formlib.form.Fields(self._subscription_field)
             self.form_fields += formlib.form.Fields(
                 self._bug_notification_level_field)
             self.form_fields['bug_notification_level'].custom_widget = (
                 CustomWidgetFactory(RadioWidget))
         else:
-            self.form_fields += formlib.form.Fields(subscription_field)
+            self.form_fields += formlib.form.Fields(self._subscription_field)
         self.form_fields['subscription'].custom_widget = CustomWidgetFactory(
             RadioWidget)
 
@@ -225,19 +250,16 @@
         """See `LaunchpadFormView`."""
         super(BugSubscriptionSubscribeSelfView, self).setUpWidgets()
         if self._use_advanced_features:
-            if self.user_is_subscribed:
-                self.widgets['bug_notification_level'].visible = False
+            if self._subscriber_count_for_current_user == 0:
+                # We hide the subscription widget if the user isn't
+                # subscribed, since we know who the subscriber is and we
+                # don't need to present them with a single radio button.
+                self.widgets['subscription'].visible = False
             else:
-                if self._subscriber_count_for_current_user == 0:
-                    # We hide the subscription widget if the user isn't
-                    # subscribed, since we know who the subscriber is and we
-                    # don't need to present them with a single radio button.
-                    self.widgets['subscription'].visible = False
-                else:
-                    # We show the subscription widget when the user is
-                    # subscribed via a team, because they can either
-                    # subscribe theirself or unsubscribe their team.
-                    self.widgets['subscription'].visible = True
+                # We show the subscription widget when the user is
+                # subscribed via a team, because they can either
+                # subscribe theirself or unsubscribe their team.
+                self.widgets['subscription'].visible = True
 
     @cachedproperty
     def user_is_subscribed(self):
@@ -266,12 +288,16 @@
     def subscribe_action(self, action, data):
         """Handle subscription requests."""
         subscription_person = self.widgets['subscription'].getInputValue()
-        if (not self.user_is_subscribed and
+        if self._use_advanced_features:
+            bug_notification_level = data.get('bug_notification_level', None)
+        else:
+            bug_notification_level = None
+
+        if (subscription_person == self._update_subscription_term.value and
+            self.user_is_subscribed):
+            self._handleUpdateSubscription(level=bug_notification_level)
+        elif (not self.user_is_subscribed and
             (subscription_person == self.user)):
-            if self._use_advanced_features:
-                bug_notification_level = data['bug_notification_level']
-            else:
-                bug_notification_level = None
             self._handleSubscribe(bug_notification_level)
         else:
             self._handleUnsubscribe(subscription_person)
@@ -296,7 +322,6 @@
         when the bug is private. The user must be unsubscribed from all dupes
         too, or they would keep getting mail about this bug!
         """
-
         # ** Important ** We call unsubscribeFromDupes() before
         # unsubscribe(), because if the bug is private, the current user
         # will be prevented from calling methods on the main bug after
@@ -331,6 +356,13 @@
             structured(
                 self._getUnsubscribeNotification(user, unsubed_dupes)))
 
+    def _handleUpdateSubscription(self, level):
+        """Handle updating a user's subscription."""
+        subscription = self.current_user_subscription
+        subscription.bug_notification_level = level
+        self.request.response.addNotification(
+            "Your subscription to this bug has been updated.")
+
     def _getUnsubscribeNotification(self, user, unsubed_dupes):
         """Construct and return the unsubscribe-from-bug feedback message.
 

=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py	2010-11-02 12:00:57 +0000
+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py	2010-11-03 14:03:50 +0000
@@ -5,13 +5,10 @@
 
 __metaclass__ = type
 
-from storm.store import Store
-
 from canonical.launchpad.ftests import LaunchpadFormHarness
 from canonical.testing.layers import LaunchpadFunctionalLayer
 
 from lp.bugs.browser.bugsubscription import BugSubscriptionSubscribeSelfView
-from lp.bugs.model.bugsubscription import BugSubscription
 from lp.registry.enum import BugNotificationLevel
 from lp.testing import (
     feature_flags,
@@ -25,13 +22,10 @@
 
     layer = LaunchpadFunctionalLayer
 
-    def _getBugSubscriptionForUserAndBug(self, user, bug):
-        """Return the BugSubscription for a given user, bug combination."""
-        store = Store.of(bug)
-        return store.find(
-            BugSubscription,
-            BugSubscription.person == user,
-            BugSubscription.bug == bug).one()
+    def setUp(self):
+        super(BugSubscriptionAdvancedFeaturesTestCase, self).setUp()
+        with feature_flags():
+            set_feature_flag(u'malone.advanced-subscriptions.enabled', u'on')
 
     def test_subscribe_uses_bug_notification_level(self):
         # When a user subscribes to a bug using the advanced features on
@@ -46,7 +40,6 @@
         # We don't display BugNotificationLevel.NOTHING as an option.
         # This is tested below.
         with feature_flags():
-            set_feature_flag(u'malone.advanced-subscriptions.enabled', u'on')
             displayed_levels = [
                 level for level in BugNotificationLevel.items
                 if level != BugNotificationLevel.NOTHING]
@@ -61,8 +54,7 @@
                         }
                     harness.submit('continue', form_data)
 
-        subscription = self._getBugSubscriptionForUserAndBug(
-            person, bug)
+        subscription = bug.getSubscriptionForPerson(person)
         self.assertEqual(
             level, subscription.bug_notification_level,
             "Bug notification level of subscription should be %s, is "
@@ -75,7 +67,6 @@
         bug = self.factory.makeBug()
         person = self.factory.makePerson()
         with feature_flags():
-            set_feature_flag(u'malone.advanced-subscriptions.enabled', u'on')
             with person_logged_in(person):
                 level = BugNotificationLevel.NOTHING
                 harness = LaunchpadFormHarness(
@@ -91,3 +82,86 @@
                     harness.getFieldError('bug_notification_level'),
                     "The view should treat BugNotificationLevel.NOTHING "
                     "as an invalid value.")
+
+    def test_user_can_update_subscription(self):
+        # A user can update their bug subscription using the
+        # BugSubscriptionSubscribeSelfView.
+        bug = self.factory.makeBug()
+        person = self.factory.makePerson()
+        with feature_flags():
+            with person_logged_in(person):
+                bug.subscribe(person, person, BugNotificationLevel.COMMENTS)
+                # Now the person updates their subscription so they're
+                # subscribed at the METADATA level.
+                level = BugNotificationLevel.METADATA
+                harness = LaunchpadFormHarness(
+                    bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                form_data = {
+                    'field.subscription': 'update-subscription',
+                    'field.bug_notification_level': level.name,
+                    }
+                harness.submit('continue', form_data)
+                self.assertFalse(harness.hasErrors())
+
+        subscription = bug.getSubscriptionForPerson(person)
+        self.assertEqual(
+            BugNotificationLevel.METADATA,
+            subscription.bug_notification_level,
+            "Bug notification level of subscription should be METADATA, is "
+            "actually %s." % subscription.bug_notification_level.name)
+
+    def test_user_can_unsubscribe(self):
+        # A user can unsubscribe from a bug using the
+        # BugSubscriptionSubscribeSelfView.
+        bug = self.factory.makeBug()
+        person = self.factory.makePerson()
+        with feature_flags():
+            with person_logged_in(person):
+                bug.subscribe(person, person)
+                harness = LaunchpadFormHarness(
+                    bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                form_data = {
+                    'field.subscription': person.name,
+                    }
+                harness.submit('continue', form_data)
+
+        subscription = bug.getSubscriptionForPerson(person)
+        self.assertIs(
+            None, subscription,
+            "There should be no BugSubscription for this person.")
+
+    def test_field_values_set_correctly_for_existing_subscriptions(self):
+        # When a user who is already subscribed to a bug visits the
+        # BugSubscriptionSubscribeSelfView, its bug_notification_level
+        # field will be set according to their current susbscription
+        # level.
+        bug = self.factory.makeBug()
+        person = self.factory.makePerson()
+        with feature_flags():
+            with person_logged_in(person):
+                # We subscribe using the harness rather than doing it
+                # directly so that we don't have to commit() between
+                # subscribing and checking the default value.
+                level = BugNotificationLevel.METADATA
+                harness = LaunchpadFormHarness(
+                    bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                form_data = {
+                    'field.subscription': person.name,
+                    'field.bug_notification_level': level.name,
+                    }
+                harness.submit('continue', form_data)
+
+                # The default value for the bug_notification_level field
+                # should now be the same as the level used to subscribe
+                # above.
+                harness = LaunchpadFormHarness(
+                    bug.default_bugtask, BugSubscriptionSubscribeSelfView)
+                bug_notification_level_widget = (
+                    harness.view.widgets['bug_notification_level'])
+                default_notification_level_value = (
+                    bug_notification_level_widget._getDefault())
+                self.assertEqual(
+                    BugNotificationLevel.METADATA,
+                    default_notification_level_value,
+                    "Default value for bug_notification_level should be "
+                    "METADATA, is actually %s" % default_notification_level_value)

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2010-10-31 20:18:45 +0000
+++ lib/lp/bugs/configure.zcml	2010-11-03 14:03:50 +0000
@@ -694,6 +694,7 @@
                     getSubscribersFromDuplicates
                     getSubscriptionsFromDuplicates
                     getSubscribersForPerson
+                    getSubscriptionForPerson
                     indexed_messages
                     getAlsoNotifiedSubscribers
                     getBugWatch

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2010-10-31 20:18:45 +0000
+++ lib/lp/bugs/interfaces/bug.py	2010-11-03 14:03:50 +0000
@@ -505,6 +505,12 @@
         :return: An IResultSet.
         """
 
+    def getSubscriptionForPerson(person):
+        """Return the `BugSubscription` for a `Person` to this `Bug`.
+
+        If no such `BugSubscription` exists, return None.
+        """
+
     def getBugNotificationRecipients(duplicateof=None, old_bug=None,
                                      include_master_dupe_subscribers=False):
         """Return a complete INotificationRecipientSet instance.

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2010-10-31 20:18:45 +0000
+++ lib/lp/bugs/model/bug.py	2010-11-03 14:03:50 +0000
@@ -954,6 +954,14 @@
             ).order_by(Person.name),
             cache_subscriber, pre_iter_hook=cache_unsubscribed)
 
+    def getSubscriptionForPerson(self, person):
+        """See `IBug`."""
+        store = Store.of(self)
+        return store.find(
+            BugSubscription,
+            BugSubscription.person == person,
+            BugSubscription.bug == self).one()
+
     def getAlsoNotifiedSubscribers(self, recipients=None, level=None):
         """See `IBug`.
 


Follow ups