← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/make-person-visibility-mutate into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/make-person-visibility-mutate into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #928440 in Launchpad itself: "When attempting to create a new team, I'm told I am "Not allowed here""
  https://bugs.launchpad.net/launchpad/+bug/928440

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/make-person-visibility-mutate/+merge/92715

Recently I landed a branch that allowed an owner of a team with a commercial subscription to create private teams and change the visibility of existing teams.

However, there was a great, big glaring problem with that. Namely, to edit the visibility attribute on ITeam required the launchpad.Commercial permission.

This branch changes that, visibility is now a property on IPersonPublic, and I've added a mutator method that checks if the calling user has a current commercial subscription if the feature flag is on, and falls back to launchpad.Commercial if it is not.

I have also cleaned up some lint, and some import crazyness in lp.registry.browser.team.
-- 
https://code.launchpad.net/~stevenk/launchpad/make-person-visibility-mutate/+merge/92715
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/make-person-visibility-mutate into lp:launchpad.
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py	2012-02-06 14:39:29 +0000
+++ lib/lp/registry/browser/team.py	2012-02-13 05:42:21 +0000
@@ -43,6 +43,7 @@
 import math
 from urllib import unquote
 
+from lazr.restful.interface import copy_field
 from lazr.restful.interfaces import IJSONRequestCache
 from lazr.restful.utils import smartquote
 import pytz
@@ -50,8 +51,11 @@
 from z3c.ptcompat import ViewPageTemplateFile
 from zope.app.form.browser import TextAreaWidget
 from zope.component import getUtility
-from zope.formlib import form
-from zope.formlib.form import FormFields
+from zope.formlib.form import (
+    Fields,
+    FormField,
+    FormFields,
+    )
 from zope.interface import (
     classImplements,
     implements,
@@ -311,6 +315,10 @@
         self.field_names.remove('teamowner')
         super(TeamEditView, self).setUpFields()
         self.conditionallyOmitVisibility()
+        if 'visibility' in [field.__name__ for field in self.form_fields]:
+            self.form_fields = self.form_fields.omit('visibility')
+            visibility = copy_field(ITeam['visibility'], readonly=False)
+            self.form_fields += Fields(visibility)
 
     def setUpWidgets(self):
         super(TeamEditView, self).setUpWidgets()
@@ -348,6 +356,9 @@
     @action('Save', name='save')
     def action_save(self, action, data):
         try:
+            visibility = data.get('visibility')
+            if visibility:
+                self.context.transitionVisibility(visibility, self.user)
             self.updateContextFromData(data)
         except ImmutableVisibilityError, error:
             self.request.response.addErrorNotification(str(error))
@@ -449,7 +460,7 @@
 
         # Replace the default contact_method field by a custom one.
         self.form_fields = (
-            form.FormFields(self.getContactMethodField())
+            FormFields(self.getContactMethodField())
             + self.form_fields.omit('contact_method'))
 
     def getContactMethodField(self):
@@ -482,7 +493,7 @@
             # field.
             del terms[hosted_list_term_index]
 
-        return form.FormField(
+        return FormField(
             Choice(__name__='contact_method',
                    title=_("How do people contact this team's members?"),
                    required=True, vocabulary=SimpleVocabulary(terms)))
@@ -990,7 +1001,6 @@
 
     page_title = 'Register a new team in Launchpad'
     label = page_title
-    schema = ITeamCreation
 
     custom_widget('teamowner', HiddenUserWidget)
     custom_widget(
@@ -1000,6 +1010,9 @@
         orientation='vertical')
     custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
 
+    class schema(ITeamCreation):
+        visibility = copy_field(ITeamCreation['visibility'], readonly=False)
+
     def setUpFields(self):
         """See `LaunchpadViewForm`.
 
@@ -1022,7 +1035,7 @@
             subscriptionpolicy, defaultmembershipperiod, defaultrenewalperiod)
         visibility = data.get('visibility')
         if visibility:
-            team.visibility = visibility
+            team.transitionVisibility(visibility, self.user)
         email = data.get('contactemail')
         if email is not None:
             generateTokenAndValidationEmail(email, team)

=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py	2012-02-10 07:17:58 +0000
+++ lib/lp/registry/browser/tests/test_team.py	2012-02-13 05:42:21 +0000
@@ -519,6 +519,32 @@
                     'visibility',
                     [field.__name__ for field in view.form_fields])
 
+<<<<<<< TREE
+=======
+    def test_person_with_cs_can_create_private_team(self):
+        personset = getUtility(IPersonSet)
+        team = self.factory.makeTeam(
+            subscription_policy=TeamSubscriptionPolicy.MODERATED)
+        product = self.factory.makeProduct(owner=team)
+        self.factory.makeCommercialSubscription(product)
+        team_name = self.factory.getUniqueString()
+        form = {
+            'field.name': team_name,
+            'field.displayname': 'New Team',
+            'field.subscriptionpolicy': 'RESTRICTED',
+            'field.visibility': 'PRIVATE',
+            'field.actions.create': 'Create',
+            }
+        with person_logged_in(team.teamowner):
+            with FeatureFixture(self.feature_flag):
+                view = create_initialized_view(
+                    personset, name=self.view_name, principal=team.teamowner,
+                    form=form)
+            team = personset.getByName(team_name)
+            self.assertIsNotNone(team)
+            self.assertEqual(PersonVisibility.PRIVATE, team.visibility)
+
+>>>>>>> MERGE-SOURCE
     def test_person_with_expired_cs_does_not_see_visibility(self):
         personset = getUtility(IPersonSet)
         team = self.factory.makeTeam(

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-02-08 06:00:46 +0000
+++ lib/lp/registry/configure.zcml	2012-02-13 05:42:21 +0000
@@ -906,8 +906,6 @@
                 interface="lp.registry.interfaces.person.IPersonPublic"/>
             <allow
                 interface="lp.registry.interfaces.person.IHasStanding"/>
-            <allow
-                interface="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
             <require
                 permission="launchpad.LimitedView"
                 interface="lp.registry.interfaces.person.IPersonLimitedView"/>
@@ -928,14 +926,11 @@
                 set_schema="lp.registry.interfaces.person.IPersonViewRestricted
                             lp.registry.interfaces.person.IPersonPublic
                             lp.registry.interfaces.person.ITeamPublic"
-                set_attributes="displayname icon logo"/>
+                set_attributes="displayname icon logo visibility"/>
             <require
                 permission="launchpad.Moderate"
                 set_attributes="name"/>
             <require
-                permission="launchpad.Commercial"
-                set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
-            <require
                 permission="launchpad.Special"
                 interface="lp.registry.interfaces.person.IPersonSpecialRestricted"/>
             <require

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/interfaces/person.py	2012-02-13 05:42:21 +0000
@@ -44,6 +44,8 @@
     'validate_subscription_policy',
     ]
 
+import httplib
+
 from lazr.enum import (
     DBEnumeratedType,
     DBItem,
@@ -54,6 +56,7 @@
 from lazr.restful.declarations import (
     call_with,
     collection_default_content,
+    error_status,
     export_as_webservice_collection,
     export_as_webservice_entry,
     export_factory_operation,
@@ -61,6 +64,7 @@
     export_write_operation,
     exported,
     LAZR_WEBSERVICE_EXPORTED,
+    mutator_for,
     operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
@@ -690,10 +694,33 @@
     account_status_comment = Text(
         title=_("Why are you deactivating your account?"), required=False,
         readonly=True)
+    visibility = exported(
+        Choice(title=_("Visibility"),
+               description=_(
+                   "Public visibility is standard.  "
+                   "Private means the team is completely "
+                   "hidden."),
+               required=True, vocabulary=PersonVisibility,
+               default=PersonVisibility.PUBLIC, readonly=True))
 
     def anyone_can_join():
         """Quick check as to whether a team allows anyone to join."""
 
+    @mutator_for(visibility)
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(visibility=copy_field(visibility))
+    @export_write_operation()
+    @operation_for_version("beta")
+    def transitionVisibility(visibility, user):
+        """Set visibility of IPerson.
+    
+        :param visibility: The PersonVisibility to change to.
+        :param user: The user requesting the change.
+        :raises ImmutableVisibilityError: Raised when the visibility can not
+        be changed.
+        :return: None.
+        """
+
 
 class IPersonLimitedView(IHasIcon, IHasLogo):
     """IPerson attributes that require launchpad.LimitedView permission."""
@@ -1801,19 +1828,6 @@
         """
 
 
-class IPersonCommAdminWriteRestricted(Interface):
-    """IPerson attributes that require launchpad.Admin permission to set."""
-
-    visibility = exported(
-        Choice(title=_("Visibility"),
-               description=_(
-                   "Public visibility is standard.  "
-                   "Private means the team is completely "
-                   "hidden."),
-               required=True, vocabulary=PersonVisibility,
-               default=PersonVisibility.PUBLIC))
-
-
 class IPersonSpecialRestricted(Interface):
     """IPerson methods that require launchpad.Special permission to use."""
 
@@ -1873,9 +1887,8 @@
 
 
 class IPerson(IPersonPublic, IPersonLimitedView, IPersonViewRestricted,
-              IPersonEditRestricted, IPersonCommAdminWriteRestricted,
-              IPersonSpecialRestricted, IHasStanding, ISetLocation,
-              IRootContext):
+              IPersonEditRestricted, IPersonSpecialRestricted, IHasStanding,
+              ISetLocation, IRootContext):
     """A Person."""
     export_as_webservice_entry(plural_name='people')
 
@@ -2536,6 +2549,7 @@
     """XMLRPC application root for ISoftwareCenterAgentAPI."""
 
 
+@error_status(httplib.FORBIDDEN)
 class ImmutableVisibilityError(Exception):
     """A change in team membership visibility is not allowed."""
 

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/model/person.py	2012-02-13 05:42:21 +0000
@@ -243,6 +243,7 @@
     SQLBase,
     sqlvalues,
     )
+from lp.services.features import getFeatureFlag
 from lp.services.helpers import (
     ensure_unicode,
     shortlist,
@@ -289,6 +290,7 @@
 from lp.services.verification.interfaces.authtoken import LoginTokenType
 from lp.services.verification.interfaces.logintoken import ILoginTokenSet
 from lp.services.verification.model.logintoken import LoginToken
+from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.dbpolicy import MasterDatabasePolicy
 from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.worlddata.model.language import Language
@@ -3062,6 +3064,18 @@
         """See `IPerson.`"""
         return self.subscriptionpolicy in CLOSED_TEAM_POLICY
 
+    def transitionVisibility(self, visibility, user):
+        validate_person_visibility(self, 'visibility', visibility)
+        feature_flag = getFeatureFlag(
+            'disclosure.show_visibility_for_team_add.enabled')
+        if feature_flag:
+            if not user.hasCurrentCommercialSubscription():
+                raise ImmutableVisibilityError()
+        else:
+            if not check_permission('launchpad.Commercial', self):
+                raise ImmutableVisibilityError()
+        self.visibility = visibility
+
 
 class PersonSet:
     """The set of persons."""

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2012-02-03 11:23:30 +0000
+++ lib/lp/registry/tests/test_person.py	2012-02-13 05:42:21 +0000
@@ -538,7 +538,7 @@
         self.assertFalse(person.isAnySecurityContact())
 
     def test_has_current_commercial_subscription(self):
-        # IPerson.hasCurrentCommercialSubscription() checks for one. 
+        # IPerson.hasCurrentCommercialSubscription() checks for one.
         team = self.factory.makeTeam(
             subscription_policy=TeamSubscriptionPolicy.MODERATED)
         product = self.factory.makeProduct(owner=team)
@@ -560,6 +560,12 @@
         person = self.factory.makePerson()
         self.assertFalse(person.hasCurrentCommercialSubscription())
 
+    def test_can_not_set_visibility(self):
+        person = self.factory.makePerson()
+        self.assertRaises(
+            ImmutableVisibilityError, person.transitionVisibility,
+            PersonVisibility.PRIVATE)
+
 
 class TestPersonStates(TestCaseWithFactory):