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

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 14791
Proposed branch: lp:~stevenk/launchpad/make-person-visibility-mutate
Merge into: lp:launchpad
Diff against target: 398 lines (+118/-41)
8 files modified
lib/lp/registry/browser/team.py (+22/-17)
lib/lp/registry/browser/tests/test_team.py (+23/-0)
lib/lp/registry/configure.zcml (+1/-6)
lib/lp/registry/interfaces/person.py (+37/-16)
lib/lp/registry/model/person.py (+21/-0)
lib/lp/registry/stories/webservice/xx-private-team.txt (+1/-1)
lib/lp/registry/tests/test_errors.py (+6/-0)
lib/lp/registry/tests/test_person.py (+7/-1)
To merge this branch: bzr merge lp:~stevenk/launchpad/make-person-visibility-mutate
Reviewer Review Type Date Requested Status
Ian Booth (community) Approve
Review via email: mp+92715@code.launchpad.net

Commit message

[r=wallyworld][bug=928440] No longer require launchpad.Commercial to edit IPerson.visibility; use a mutator to allow any user with a current commercial subscription to do so.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Looks good. Thanks for making the suggested changes.

With the new error type FORBIDDEN added to the ImmutableVisibilityError, you should add a test like in registry/test_errors.py

    def test_ImmutableVisibilityError_forbidden(self):
        error_view = create_webservice_error_view(
            ImmutableVisibilityError())
        self.assertEqual(FORBIDDEN, error_view.status)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/team.py'
2--- lib/lp/registry/browser/team.py 2012-02-06 14:39:29 +0000
3+++ lib/lp/registry/browser/team.py 2012-02-13 23:06:19 +0000
4@@ -43,6 +43,7 @@
5 import math
6 from urllib import unquote
7
8+from lazr.restful.interface import copy_field
9 from lazr.restful.interfaces import IJSONRequestCache
10 from lazr.restful.utils import smartquote
11 import pytz
12@@ -50,8 +51,11 @@
13 from z3c.ptcompat import ViewPageTemplateFile
14 from zope.app.form.browser import TextAreaWidget
15 from zope.component import getUtility
16-from zope.formlib import form
17-from zope.formlib.form import FormFields
18+from zope.formlib.form import (
19+ Fields,
20+ FormField,
21+ FormFields,
22+ )
23 from zope.interface import (
24 classImplements,
25 implements,
26@@ -142,7 +146,6 @@
27 )
28 from lp.security import ModerateByRegistryExpertsOrAdmins
29 from lp.services.config import config
30-from lp.services.features import getFeatureFlag
31 from lp.services.fields import PublicPersonChoice
32 from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
33 from lp.services.privacy.interfaces import IObjectPrivacy
34@@ -269,15 +272,12 @@
35 'Private teams must have a Restricted subscription '
36 'policy.')
37
38- def conditionallyOmitVisibility(self):
39- """Remove the visibility field if not authorized."""
40- if check_permission('launchpad.Commercial', self.context):
41- return
42- feature_flag = getFeatureFlag(
43- 'disclosure.show_visibility_for_team_add.enabled')
44- if feature_flag and self.user.hasCurrentCommercialSubscription():
45- return
46+ def setUpVisibilityField(self):
47+ """Set the visibility field to read-write, or remove it."""
48 self.form_fields = self.form_fields.omit('visibility')
49+ if self.user and self.user.checkAllowVisibility():
50+ visibility = copy_field(ITeam['visibility'], readonly=False)
51+ self.form_fields += Fields(visibility)
52
53
54 class TeamEditView(TeamFormMixin, PersonRenameFormMixin,
55@@ -310,7 +310,7 @@
56 self.field_names.remove('contactemail')
57 self.field_names.remove('teamowner')
58 super(TeamEditView, self).setUpFields()
59- self.conditionallyOmitVisibility()
60+ self.setUpVisibilityField()
61
62 def setUpWidgets(self):
63 super(TeamEditView, self).setUpWidgets()
64@@ -348,6 +348,10 @@
65 @action('Save', name='save')
66 def action_save(self, action, data):
67 try:
68+ visibility = data.get('visibility')
69+ if visibility:
70+ self.context.transitionVisibility(visibility, self.user)
71+ del data['visibility']
72 self.updateContextFromData(data)
73 except ImmutableVisibilityError, error:
74 self.request.response.addErrorNotification(str(error))
75@@ -449,7 +453,7 @@
76
77 # Replace the default contact_method field by a custom one.
78 self.form_fields = (
79- form.FormFields(self.getContactMethodField())
80+ FormFields(self.getContactMethodField())
81 + self.form_fields.omit('contact_method'))
82
83 def getContactMethodField(self):
84@@ -482,7 +486,7 @@
85 # field.
86 del terms[hosted_list_term_index]
87
88- return form.FormField(
89+ return FormField(
90 Choice(__name__='contact_method',
91 title=_("How do people contact this team's members?"),
92 required=True, vocabulary=SimpleVocabulary(terms)))
93@@ -988,9 +992,9 @@
94 class TeamAddView(TeamFormMixin, HasRenewalPolicyMixin, LaunchpadFormView):
95 """View for adding a new team."""
96
97+ schema = ITeamCreation
98 page_title = 'Register a new team in Launchpad'
99 label = page_title
100- schema = ITeamCreation
101
102 custom_widget('teamowner', HiddenUserWidget)
103 custom_widget(
104@@ -1006,7 +1010,7 @@
105 Only Launchpad Admins get to see the visibility field.
106 """
107 super(TeamAddView, self).setUpFields()
108- self.conditionallyOmitVisibility()
109+ self.setUpVisibilityField()
110
111 @action('Create Team', name='create')
112 def create_action(self, action, data):
113@@ -1022,7 +1026,8 @@
114 subscriptionpolicy, defaultmembershipperiod, defaultrenewalperiod)
115 visibility = data.get('visibility')
116 if visibility:
117- team.visibility = visibility
118+ team.transitionVisibility(visibility, self.user)
119+ del data['visibility']
120 email = data.get('contactemail')
121 if email is not None:
122 generateTokenAndValidationEmail(email, team)
123
124=== modified file 'lib/lp/registry/browser/tests/test_team.py'
125--- lib/lp/registry/browser/tests/test_team.py 2012-02-10 07:17:58 +0000
126+++ lib/lp/registry/browser/tests/test_team.py 2012-02-13 23:06:19 +0000
127@@ -519,6 +519,29 @@
128 'visibility',
129 [field.__name__ for field in view.form_fields])
130
131+ def test_person_with_cs_can_create_private_team(self):
132+ personset = getUtility(IPersonSet)
133+ team = self.factory.makeTeam(
134+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
135+ product = self.factory.makeProduct(owner=team)
136+ self.factory.makeCommercialSubscription(product)
137+ team_name = self.factory.getUniqueString()
138+ form = {
139+ 'field.name': team_name,
140+ 'field.displayname': 'New Team',
141+ 'field.subscriptionpolicy': 'RESTRICTED',
142+ 'field.visibility': 'PRIVATE',
143+ 'field.actions.create': 'Create',
144+ }
145+ with person_logged_in(team.teamowner):
146+ with FeatureFixture(self.feature_flag):
147+ view = create_initialized_view(
148+ personset, name=self.view_name, principal=team.teamowner,
149+ form=form)
150+ team = personset.getByName(team_name)
151+ self.assertIsNotNone(team)
152+ self.assertEqual(PersonVisibility.PRIVATE, team.visibility)
153+
154 def test_person_with_expired_cs_does_not_see_visibility(self):
155 personset = getUtility(IPersonSet)
156 team = self.factory.makeTeam(
157
158=== modified file 'lib/lp/registry/configure.zcml'
159--- lib/lp/registry/configure.zcml 2012-02-08 06:00:46 +0000
160+++ lib/lp/registry/configure.zcml 2012-02-13 23:06:19 +0000
161@@ -906,8 +906,6 @@
162 interface="lp.registry.interfaces.person.IPersonPublic"/>
163 <allow
164 interface="lp.registry.interfaces.person.IHasStanding"/>
165- <allow
166- interface="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
167 <require
168 permission="launchpad.LimitedView"
169 interface="lp.registry.interfaces.person.IPersonLimitedView"/>
170@@ -928,14 +926,11 @@
171 set_schema="lp.registry.interfaces.person.IPersonViewRestricted
172 lp.registry.interfaces.person.IPersonPublic
173 lp.registry.interfaces.person.ITeamPublic"
174- set_attributes="displayname icon logo"/>
175+ set_attributes="displayname icon logo visibility"/>
176 <require
177 permission="launchpad.Moderate"
178 set_attributes="name"/>
179 <require
180- permission="launchpad.Commercial"
181- set_schema="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
182- <require
183 permission="launchpad.Special"
184 interface="lp.registry.interfaces.person.IPersonSpecialRestricted"/>
185 <require
186
187=== modified file 'lib/lp/registry/interfaces/person.py'
188--- lib/lp/registry/interfaces/person.py 2012-02-03 11:23:30 +0000
189+++ lib/lp/registry/interfaces/person.py 2012-02-13 23:06:19 +0000
190@@ -44,6 +44,8 @@
191 'validate_subscription_policy',
192 ]
193
194+import httplib
195+
196 from lazr.enum import (
197 DBEnumeratedType,
198 DBItem,
199@@ -54,6 +56,7 @@
200 from lazr.restful.declarations import (
201 call_with,
202 collection_default_content,
203+ error_status,
204 export_as_webservice_collection,
205 export_as_webservice_entry,
206 export_factory_operation,
207@@ -61,6 +64,7 @@
208 export_write_operation,
209 exported,
210 LAZR_WEBSERVICE_EXPORTED,
211+ mutator_for,
212 operation_for_version,
213 operation_parameters,
214 operation_returns_collection_of,
215@@ -690,10 +694,40 @@
216 account_status_comment = Text(
217 title=_("Why are you deactivating your account?"), required=False,
218 readonly=True)
219+ visibility = exported(
220+ Choice(title=_("Visibility"),
221+ description=_(
222+ "Public visibility is standard. "
223+ "Private means the team is completely "
224+ "hidden."),
225+ required=True, vocabulary=PersonVisibility,
226+ default=PersonVisibility.PUBLIC, readonly=True))
227
228 def anyone_can_join():
229 """Quick check as to whether a team allows anyone to join."""
230
231+ def checkAllowVisibility():
232+ """Is the user allowed to see the visibility field.
233+
234+ :param: The user.
235+ :return: True if they can, otherwise False.
236+ """
237+
238+ @mutator_for(visibility)
239+ @call_with(user=REQUEST_USER)
240+ @operation_parameters(visibility=copy_field(visibility))
241+ @export_write_operation()
242+ @operation_for_version("beta")
243+ def transitionVisibility(visibility, user):
244+ """Set visibility of IPerson.
245+
246+ :param visibility: The PersonVisibility to change to.
247+ :param user: The user requesting the change.
248+ :raises: `ImmutableVisibilityError` when the visibility can not
249+ be changed.
250+ :return: None.
251+ """
252+
253
254 class IPersonLimitedView(IHasIcon, IHasLogo):
255 """IPerson attributes that require launchpad.LimitedView permission."""
256@@ -1801,19 +1835,6 @@
257 """
258
259
260-class IPersonCommAdminWriteRestricted(Interface):
261- """IPerson attributes that require launchpad.Admin permission to set."""
262-
263- visibility = exported(
264- Choice(title=_("Visibility"),
265- description=_(
266- "Public visibility is standard. "
267- "Private means the team is completely "
268- "hidden."),
269- required=True, vocabulary=PersonVisibility,
270- default=PersonVisibility.PUBLIC))
271-
272-
273 class IPersonSpecialRestricted(Interface):
274 """IPerson methods that require launchpad.Special permission to use."""
275
276@@ -1873,9 +1894,8 @@
277
278
279 class IPerson(IPersonPublic, IPersonLimitedView, IPersonViewRestricted,
280- IPersonEditRestricted, IPersonCommAdminWriteRestricted,
281- IPersonSpecialRestricted, IHasStanding, ISetLocation,
282- IRootContext):
283+ IPersonEditRestricted, IPersonSpecialRestricted, IHasStanding,
284+ ISetLocation, IRootContext):
285 """A Person."""
286 export_as_webservice_entry(plural_name='people')
287
288@@ -2536,6 +2556,7 @@
289 """XMLRPC application root for ISoftwareCenterAgentAPI."""
290
291
292+@error_status(httplib.FORBIDDEN)
293 class ImmutableVisibilityError(Exception):
294 """A change in team membership visibility is not allowed."""
295
296
297=== modified file 'lib/lp/registry/model/person.py'
298--- lib/lp/registry/model/person.py 2012-02-03 11:23:30 +0000
299+++ lib/lp/registry/model/person.py 2012-02-13 23:06:19 +0000
300@@ -243,6 +243,7 @@
301 SQLBase,
302 sqlvalues,
303 )
304+from lp.services.features import getFeatureFlag
305 from lp.services.helpers import (
306 ensure_unicode,
307 shortlist,
308@@ -289,6 +290,7 @@
309 from lp.services.verification.interfaces.authtoken import LoginTokenType
310 from lp.services.verification.interfaces.logintoken import ILoginTokenSet
311 from lp.services.verification.model.logintoken import LoginToken
312+from lp.services.webapp.authorization import check_permission
313 from lp.services.webapp.dbpolicy import MasterDatabasePolicy
314 from lp.services.webapp.interfaces import ILaunchBag
315 from lp.services.worlddata.model.language import Language
316@@ -3062,6 +3064,25 @@
317 """See `IPerson.`"""
318 return self.subscriptionpolicy in CLOSED_TEAM_POLICY
319
320+ def checkAllowVisibility(self):
321+ feature_flag = getFeatureFlag(
322+ 'disclosure.show_visibility_for_team_add.enabled')
323+ if feature_flag:
324+ if self.hasCurrentCommercialSubscription():
325+ return True
326+ else:
327+ if check_permission('launchpad.Commercial', self):
328+ return True
329+ return False
330+
331+ def transitionVisibility(self, visibility, user):
332+ if self.visibility == visibility:
333+ return
334+ validate_person_visibility(self, 'visibility', visibility)
335+ if not user.checkAllowVisibility():
336+ raise ImmutableVisibilityError()
337+ self.visibility = visibility
338+
339
340 class PersonSet:
341 """The set of persons."""
342
343=== modified file 'lib/lp/registry/stories/webservice/xx-private-team.txt'
344--- lib/lp/registry/stories/webservice/xx-private-team.txt 2011-12-24 17:49:30 +0000
345+++ lib/lp/registry/stories/webservice/xx-private-team.txt 2012-02-13 23:06:19 +0000
346@@ -177,5 +177,5 @@
347
348 >>> print modify_team('/~my-new-team-3', {'visibility' : 'Private'},
349 ... 'PATCH', user_webservice)
350- HTTP/1.1 401 Unauthorized
351+ HTTP/1.1 403 Forbidden
352 ...
353
354=== modified file 'lib/lp/registry/tests/test_errors.py'
355--- lib/lp/registry/tests/test_errors.py 2012-01-01 02:58:52 +0000
356+++ lib/lp/registry/tests/test_errors.py 2012-02-13 23:06:19 +0000
357@@ -28,6 +28,7 @@
358 UserCannotChangeMembershipSilently,
359 UserCannotSubscribePerson,
360 )
361+from lp.registry.interfaces.person import ImmutableVisibilityError
362 from lp.testing import TestCase
363 from lp.testing.layers import FunctionalLayer
364 from lp.testing.views import create_webservice_error_view
365@@ -90,3 +91,8 @@
366 def test_NameAlreadyTaken_bad_request(self):
367 error_view = create_webservice_error_view(NameAlreadyTaken())
368 self.assertEqual(CONFLICT, error_view.status)
369+
370+ def test_ImmutableVisibilityError_forbidden(self):
371+ error_view = create_webservice_error_view(
372+ ImmutableVisibilityError())
373+ self.assertEqual(FORBIDDEN, error_view.status)
374
375=== modified file 'lib/lp/registry/tests/test_person.py'
376--- lib/lp/registry/tests/test_person.py 2012-02-03 11:23:30 +0000
377+++ lib/lp/registry/tests/test_person.py 2012-02-13 23:06:19 +0000
378@@ -538,7 +538,7 @@
379 self.assertFalse(person.isAnySecurityContact())
380
381 def test_has_current_commercial_subscription(self):
382- # IPerson.hasCurrentCommercialSubscription() checks for one.
383+ # IPerson.hasCurrentCommercialSubscription() checks for one.
384 team = self.factory.makeTeam(
385 subscription_policy=TeamSubscriptionPolicy.MODERATED)
386 product = self.factory.makeProduct(owner=team)
387@@ -560,6 +560,12 @@
388 person = self.factory.makePerson()
389 self.assertFalse(person.hasCurrentCommercialSubscription())
390
391+ def test_can_not_set_visibility(self):
392+ person = self.factory.makePerson()
393+ self.assertRaises(
394+ ImmutableVisibilityError, person.transitionVisibility,
395+ PersonVisibility.PRIVATE, person)
396+
397
398 class TestPersonStates(TestCaseWithFactory):
399