Merge lp:~sinzui/launchpad/team-membership-policy-1 into lp:launchpad/db-devel

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 10172
Proposed branch: lp:~sinzui/launchpad/team-membership-policy-1
Merge into: lp:launchpad/db-devel
Diff against target: 635 lines (+247/-91)
11 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+1/-0)
lib/lp/registry/browser/person.py (+0/-21)
lib/lp/registry/browser/team.py (+8/-3)
lib/lp/registry/help/team-subscription-policy.html (+59/-0)
lib/lp/registry/interfaces/person.py (+49/-20)
lib/lp/registry/model/person.py (+2/-1)
lib/lp/registry/templates/person-macros.pt (+4/-3)
lib/lp/registry/tests/test_team.py (+116/-35)
lib/lp/registry/vocabularies.py (+4/-4)
lib/lp/soyuz/doc/archive.txt (+2/-2)
lib/lp/soyuz/model/archive.py (+2/-2)
To merge this branch: bzr merge lp:~sinzui/launchpad/team-membership-policy-1
Reviewer Review Type Date Requested Status
Matthew Revell (community) text and ui Approve
Leonard Richardson (community) Approve
Review via email: mp+48203@code.launchpad.net

Description of the change

Allow open teams to control how members join.

    Launchpad bug: https://bugs.launchpad.net/bugs/700724
    Pre-implementation: floacoste, lifeless
    Test command: ./bin/test -vv \
      -t test_person_vocabularies -t doc/vocabularies \
      -t test_team -t teammembership.txt -t team-join-views \
      -t doc/archive.txt

Ubuntu loco teams where upset by the change to ensure team membership is
not compromised. Many OPEN teams are now Moderated and the work of approving
membership is disruptive. Providing the teams with an API script to
automatically approve members undermines the need manage membership of teams
that control secured assets.

The underling issue is that ~locoteams does not own any assets that need to
be secure. It is MODERATED because it needs to manage *how* users become
members. ~locoteams delegates user membership to its member teams, and it
only manages the direct members. The team *IS* open to anyone, but guards
who is a direct member using the propose-member feature of moderated teams.

There are two kinds of closed teams: RESTRICTED and MODERATED. Their first
concern is control. They limit membership because they control secured assets.

There could be two kinds of open teams: OPEN and DELEGATED. (Maybe change
OPEN to PERMISSIVE). They encourage membership to build communities. The
DELEGATED team reviews who is a direct member to manage the community
hierarchy. OPEN has no structure.

I do not believe this issue is about adding an exception for ~locoteams or
changing security to an ad hoc team declaration. We can validation the
community need by asking if other large communities need a DELEGATED policy
to organise their hierarchy.

--------------------------------------------------------------------

RULES

    This bug:
    * Add the DELEGATED TeamSubscriptionPolicy.
      This is an enum in the DB so the change will happen in staging.
    * Revise the descriptions of all TeamSubscriptionPolicy to clarify
      why they are used.
    * Update the help text on the new team and team edit forms
    * Update the hover help text that is shown next to the subscription
      policy field on team pages
    * Update the propose-member rules to be enforced for MODERATED and
      DELEGATED teams.
    * PS. line 1969 in archive.txt sets up a test that looks insecure.
    * Update code that checks for OPEN to check for both OPEN and DELEGATED.
    * Extra credit: change the comma used in the via column on +participation
      to an arrow.
    * Update help.launchpad.net

    In a follow up bug:
    * Change ~locoteams to DELEGATED.
    * Restore the affected subteams (listed in the this bug) to OPEN.

QA

    * Create a DELEGATED team
    * Verify that users see the join the team link.
    * Have another user create an OPEN team and propose membership
    * Accept the membership
    * Verify the OPEN team can add members.
    * Verify the DELEGATED team is listed on the new member's +participation
      page.

LINT

    lib/canonical/launchpad/icing/style-3-0.css.in
    lib/lp/registry/vocabularies.py
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/team.py
    lib/lp/registry/help/team-subscription-policy.html
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py
    lib/lp/registry/templates/person-macros.pt
    lib/lp/registry/tests/test_team.py
    lib/lp/soyuz/doc/archive.txt
    lib/lp/soyuz/model/archive.py

IMPLEMENTATION

Rewrote the description of the enums and added DELEGATED. I changed the order
so that they start with the most open enum and finish with the most closed.
I added OPEN_TEAM_POLICY and CLOSED_TEAM_POLICY so that code that tests
the rules of control have a common definition of enums. I updated
subscriptionpolicy() to reuse the enum docstring.
    lib/lp/registry/interfaces/person.py

Update the code to use the common definition of open and closed teams.
The test_team diff is hard to read, so let me explain. There are no new
test methods. I refactored the test to be three tests of common, open, and
closed behaviours. I then subclasses the open and closed behaviour tests
to verify that each enum behaves as expected.
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/vocabularies.py
    lib/lp/registry/tests/test_team.py
    lib/lp/soyuz/model/archive.py

Updated the proposed rule in IPerson.join() to place users in the PROPOSED
status when a DELEGATED team is passed. I added unittest coverage for
join().
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_team.py

Removed an obsolete property to generate the enum description. The template
macro was using the enum itself. However, the description was only shown
as a tooltip for the help icon. Clicking the help icon did nothing. I moved
the tooltip to the actual text, and added a real help file and link for the
policy. I am not happy with the duplication of the enum text in the help file.
I reported bug 711358 about how we might generate help for enums.
    lib/lp/registry/browser/person.py
    lib/lp/registry/templates/person-macros.pt
    lib/lp/registry/help/team-subscription-policy.html

Fixed a common bug in form where the help text exceeds the width of form
instructions. The forms now show the enum descriptions.
    lib/canonical/launchpad/icing/style-3-0.css.in
    lib/lp/registry/browser/team.py

Fixed ambiguous documentation. The docs used an OPEN team when working with
the archive because the join() method would require a second step to approve
the member. I updated the test to use MODERATED and used the addMember()
method instead.
    lib/lp/soyuz/doc/archive.txt

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

The text and widget change is illustrated at http://people.canonical.com/~curtis/subscription-policy.png

Revision history for this message
Leonard Richardson (leonardr) wrote :

I have only minor complaints about misspellings in user-facing text and a very confusing comment, which we discussed on IRC. r=me with those fixed.

review: Approve
Revision history for this message
Matthew Revell (matthew.revell) wrote :

I like the description of the new "Delegated" policy. However, shouldn't it mention that teams with that policy cannot have PPAs or take certain roles?

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Matthew.

I revised https://help.launchpad.net/Teams/CreatingAndRunning#Subscription%20policies to state that open and delegated teams cannot have PPAs. I refrained from stating that owning branches and projects should be avoided since users do engage in the untrusted behaviour.

If you like the the changes I will update the enums and inline help file.

Revision history for this message
Matthew Revell (matthew.revell) :
review: Approve (text and ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
2--- lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-01 06:02:46 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-03 21:26:00 +0000
4@@ -813,6 +813,7 @@
5 margin-left: 2.6em;
6 }
7 .formHelp {
8+ max-width: 45em;
9 margin: 0.2em 0 0.5em 0.2em;
10 color: #777;
11 }
12
13=== modified file 'lib/lp/registry/browser/person.py'
14--- lib/lp/registry/browser/person.py 2011-02-02 15:42:48 +0000
15+++ lib/lp/registry/browser/person.py 2011-02-03 21:26:00 +0000
16@@ -3009,27 +3009,6 @@
17 profile_url.host = config.launchpad.non_restricted_hostname
18 return str(profile_url)
19
20- @property
21- def subscription_policy_description(self):
22- """Return the description of this team's subscription policy."""
23- assert self.team.isTeam(), (
24- 'This method can only be called when the context is a team.')
25- if self.team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
26- description = _(
27- "This is a restricted team; new members can only be added "
28- "by one of the team's administrators.")
29- elif self.team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
30- description = _(
31- "This is a moderated team; all subscriptions are subjected "
32- "to approval by one of the team's administrators.")
33- elif self.team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
34- description = _(
35- "This is an open team; any user can join and no approval "
36- "is required.")
37- else:
38- raise AssertionError('Unknown subscription policy.')
39- return description
40-
41 def getURLToAssignedBugsInProgress(self):
42 """Return an URL to a page which lists all bugs assigned to this
43 person that are In Progress.
44
45=== modified file 'lib/lp/registry/browser/team.py'
46--- lib/lp/registry/browser/team.py 2011-02-02 15:43:31 +0000
47+++ lib/lp/registry/browser/team.py 2011-02-03 21:26:00 +0000
48@@ -65,7 +65,10 @@
49 )
50 from lp.app.browser.tales import PersonFormatterAPI
51 from lp.app.errors import UnexpectedFormData
52-from lp.app.widgets.itemswidgets import LaunchpadRadioWidget
53+from lp.app.widgets.itemswidgets import (
54+ LaunchpadRadioWidget,
55+ LaunchpadRadioWidgetWithDescription,
56+ )
57 from lp.app.widgets.owner import HiddenUserWidget
58 from lp.registry.browser.branding import BrandingChangeView
59 from lp.registry.interfaces.mailinglist import (
60@@ -209,7 +212,8 @@
61 custom_widget(
62 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')
63 custom_widget(
64- 'subscriptionpolicy', LaunchpadRadioWidget, orientation='vertical')
65+ 'subscriptionpolicy', LaunchpadRadioWidgetWithDescription,
66+ orientation='vertical')
67 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
68
69 def setUpFields(self):
70@@ -889,7 +893,8 @@
71 custom_widget(
72 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')
73 custom_widget(
74- 'subscriptionpolicy', LaunchpadRadioWidget, orientation='vertical')
75+ 'subscriptionpolicy', LaunchpadRadioWidgetWithDescription,
76+ orientation='vertical')
77 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
78
79 def setUpFields(self):
80
81=== added file 'lib/lp/registry/help/team-subscription-policy.html'
82--- lib/lp/registry/help/team-subscription-policy.html 1970-01-01 00:00:00 +0000
83+++ lib/lp/registry/help/team-subscription-policy.html 2011-02-03 21:26:00 +0000
84@@ -0,0 +1,59 @@
85+<html>
86+ <head>
87+ <title>Team subscription policy</title>
88+ <link rel="stylesheet" type="text/css"
89+ href="/+icing/yui/cssreset/reset.css" />
90+ <link rel="stylesheet" type="text/css"
91+ href="/+icing/yui/cssfonts/fonts.css" />
92+ <link rel="stylesheet" type="text/css"
93+ href="/+icing/yui/cssbase/base.css" />
94+ </head>
95+ <body>
96+ <h1>Team subscription policy</h1>
97+
98+ <p>
99+ There are four kinds of policy that describe who can be a member and
100+ how new memberships are handled. The choice of policy reflects the need
101+ to build a community versus the need to control Launchpad assets.
102+ </p>
103+
104+ <dl>
105+ <dt>Open</dt>
106+ <dd>
107+ Membership is open, no approval required, and subteams can be open or
108+ closed. Any user can be a member of the team and no approval is
109+ required. Subteams can be Open, Delegated, Moderated, or Restricted.
110+ Open is a good choice for encouraging a community of contributors.
111+ Open teams cannot have PPAs.
112+ </dd>
113+
114+ <dt>Delegated</dt>
115+ <dd>
116+ Membership is open, requires approval, and subteams can be open or
117+ closed. Any user can be a member of the team via a subteam, but team
118+ administrators approve direct memberships. Subteams can be Open,
119+ Delegated, Moderated, or Restricted. Delegated is a good choice for
120+ managing a large community of contributors. Delegated teams cannot
121+ have PPAs.
122+ </dd>
123+
124+ <dt>Moderated</dt>
125+ <dd>
126+ Membership is closed, requires approval, and subteams must be closed.
127+ Any user can propose a new member, but team administrators approve
128+ membership. Subteams must be Moderated or Restricted. Moderated is a
129+ good choice for teams that manage things that need to be secure, like
130+ projects, branches, or PPAs, but want to encourage users to help.
131+ </dd>
132+
133+ <dt>Restricted</dt>
134+ <dd>
135+ Membership is closed, requires approval, and subteams must be closed.
136+ Only the team's administrators can invite a user to be a member.
137+ Subteams must be Moderated or Restricted. Restricted is a good choice
138+ for teams that manage things that need to be secure, like projects,
139+ branches, or PPAs.
140+ </dd>
141+ </dl>
142+ </body>
143+</html>
144
145=== modified file 'lib/lp/registry/interfaces/person.py'
146--- lib/lp/registry/interfaces/person.py 2011-01-10 14:55:31 +0000
147+++ lib/lp/registry/interfaces/person.py 2011-02-03 21:26:00 +0000
148@@ -8,6 +8,7 @@
149 __metaclass__ = type
150
151 __all__ = [
152+ 'CLOSED_TEAM_POLICY',
153 'IAdminPeopleMergeSchema',
154 'IAdminTeamMergeSchema',
155 'IHasStanding',
156@@ -27,6 +28,7 @@
157 'ImmutableVisibilityError',
158 'InvalidName',
159 'NoSuchPerson',
160+ 'OPEN_TEAM_POLICY',
161 'PersonCreationRationale',
162 'PersonVisibility',
163 'PersonalStanding',
164@@ -392,31 +394,61 @@
165 class TeamSubscriptionPolicy(DBEnumeratedType):
166 """Team Subscription Policies
167
168- The policies that apply to a team and specify how new subscriptions must
169- be handled. More information can be found in the TeamMembershipPolicies
170- spec.
171+ The policies that describe who can be a member and how new memberships
172+ are handled. The choice of policy reflects the need to build a community
173+ versus the need to control Launchpad assets.
174 """
175
176+ OPEN = DBItem(2, """
177+ Open Team
178+
179+ Membership is open, no approval required, and subteams can be open or
180+ closed. Any user can be a member of the team and no approval is
181+ required. Subteams can be Open, Delegated, Moderated, or Restricted.
182+ Open is a good choice for encouraging a community of contributors.
183+ Open teams cannot have PPAs.
184+ """)
185+
186+ DELEGATED = DBItem(4, """
187+ Delegated Team
188+
189+ Membership is open, requires approval, and subteams can be open or
190+ closed. Any user can be a member of the team via a subteam, but team
191+ administrators approve direct memberships. Subteams can be Open,
192+ Delegated, Moderated, or Restricted. Delegated is a good choice for
193+ managing a large community of contributors. Delegated teams cannot
194+ have PPAs.
195+ """)
196+
197 MODERATED = DBItem(1, """
198 Moderated Team
199
200- All subscriptions for this team are subject to approval by one of
201- the team's administrators.
202- """)
203-
204- OPEN = DBItem(2, """
205- Open Team
206-
207- Any user can join and no approval is required.
208+ Membership is closed, requires approval, and subteams must be closed.
209+ Any user can propose a new member, but team administrators approve
210+ membership. Subteams must be Moderated or Restricted. Moderated is a
211+ good choice for teams that manage things that need to be secure, like
212+ projects, branches, or PPAs, but want to encourage users to help.
213 """)
214
215 RESTRICTED = DBItem(3, """
216 Restricted Team
217
218- New members can only be added by one of the team's administrators.
219+ Membership is closed, requires approval, and subteams must be closed.
220+ Only the team's administrators can invite a user to be a member.
221+ Subteams must be Moderated or Restricted. Restricted is a good choice
222+ for teams that manage things that need to be secure, like projects,
223+ branches, or PPAs.
224 """)
225
226
227+OPEN_TEAM_POLICY = (
228+ TeamSubscriptionPolicy.OPEN, TeamSubscriptionPolicy.DELEGATED)
229+
230+
231+CLOSED_TEAM_POLICY = (
232+ TeamSubscriptionPolicy.RESTRICTED, TeamSubscriptionPolicy.MODERATED)
233+
234+
235 class PersonVisibility(DBEnumeratedType):
236 """The visibility level of person or team objects.
237
238@@ -503,10 +535,10 @@
239 if team is None or policy == team.subscriptionpolicy:
240 # The team is being initialized or the policy is not changing.
241 return True
242- elif policy == TeamSubscriptionPolicy.OPEN:
243+ elif policy in OPEN_TEAM_POLICY:
244 # The team can be open if its super teams are open.
245 for team in team.super_teams:
246- if team.subscriptionpolicy != TeamSubscriptionPolicy.OPEN:
247+ if team.subscriptionpolicy in CLOSED_TEAM_POLICY:
248 raise TeamSubscriptionPolicyError(
249 "The team subscription policy cannot be %s because one "
250 "or more if its super teams are not open." % policy)
251@@ -516,11 +548,11 @@
252 raise TeamSubscriptionPolicyError(
253 "The team subscription policy cannot be %s because it "
254 "has one or more active PPAs." % policy)
255- elif team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
256+ elif team.subscriptionpolicy in OPEN_TEAM_POLICY:
257 # The team can become MODERATED or RESTRICTED if its member teams
258 # are not OPEN.
259 for member in team.activemembers:
260- if member.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
261+ if member.subscriptionpolicy in OPEN_TEAM_POLICY:
262 raise TeamSubscriptionPolicyError(
263 "The team subscription policy cannot be %s because one "
264 "or more if its member teams are Open." % policy)
265@@ -1770,10 +1802,7 @@
266 vocabulary=TeamSubscriptionPolicy,
267 default=TeamSubscriptionPolicy.MODERATED, required=True,
268 description=_(
269- "'Moderated' means all subscriptions must be approved. "
270- "'Open' means any user can join without approval. "
271- "'Restricted' means new members can be added only by a "
272- "team administrator.")),
273+ TeamSubscriptionPolicy.__doc__.split('\n\n')[1])),
274 exported_as='subscription_policy')
275
276 renewal_policy = exported(
277
278=== modified file 'lib/lp/registry/model/person.py'
279--- lib/lp/registry/model/person.py 2011-02-03 08:38:27 +0000
280+++ lib/lp/registry/model/person.py 2011-02-03 21:26:00 +0000
281@@ -1273,7 +1273,8 @@
282
283 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
284 raise JoinNotAllowed("This is a restricted team")
285- elif team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
286+ elif (team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED
287+ or team.subscriptionpolicy == TeamSubscriptionPolicy.DELEGATED):
288 status = proposed
289 elif team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
290 status = approved
291
292=== modified file 'lib/lp/registry/templates/person-macros.pt'
293--- lib/lp/registry/templates/person-macros.pt 2010-05-18 07:44:47 +0000
294+++ lib/lp/registry/templates/person-macros.pt 2011-02-03 21:26:00 +0000
295@@ -61,10 +61,11 @@
296 </dl>
297 <dl id="subscription-policy">
298 <dt>Subscription policy:</dt>
299- <dd>
300+ <dd
301+ tal:attributes="title context/subscriptionpolicy/description">
302 <span tal:replace="context/subscriptionpolicy/title" />
303- <img src="/@@/maybe"
304- tal:attributes="title context/subscriptionpolicy/description" />
305+ <a class="sprite maybe" href="/+help/team-subscription-policy.html"
306+ target="help">&nbsp;</a>
307 </dd>
308 </dl>
309
310
311=== modified file 'lib/lp/registry/tests/test_team.py'
312--- lib/lp/registry/tests/test_team.py 2010-12-13 18:08:19 +0000
313+++ lib/lp/registry/tests/test_team.py 2011-02-03 21:26:00 +0000
314@@ -18,7 +18,10 @@
315 FunctionalLayer,
316 )
317 from lp.registry.enum import PersonTransferJobType
318-from lp.registry.errors import TeamSubscriptionPolicyError
319+from lp.registry.errors import (
320+ JoinNotAllowed,
321+ TeamSubscriptionPolicyError,
322+ )
323 from lp.registry.interfaces.mailinglist import MailingListStatus
324 from lp.registry.interfaces.person import (
325 IPersonSet,
326@@ -220,23 +223,31 @@
327 self.assertEqual('a message', error.doc())
328
329
330-class TestTeamSubscriptionPolicyChoice(TestCaseWithFactory):
331- """Test `TeamSubsciptionPolicyChoice` constraints."""
332+class TeamSubscriptionPolicyBase(TestCaseWithFactory):
333+ """`TeamSubsciptionPolicyChoice` base test class."""
334
335 layer = DatabaseFunctionalLayer
336+ POLICY = None
337
338- def setUpTeams(self, policy, other_policy=None):
339+ def setUpTeams(self, other_policy=None):
340 if other_policy is None:
341- other_policy = policy
342- self.team = self.factory.makeTeam(subscription_policy=policy)
343+ other_policy = self.POLICY
344+ self.team = self.factory.makeTeam(subscription_policy=self.POLICY)
345 self.other_team = self.factory.makeTeam(
346 subscription_policy=other_policy, owner=self.team.teamowner)
347 self.field = ITeamPublic['subscriptionpolicy'].bind(self.team)
348 login_person(self.team.teamowner)
349
350+
351+class TestTeamSubscriptionPolicyChoiceCommon(TeamSubscriptionPolicyBase):
352+ """Test `TeamSubsciptionPolicyChoice` constraints."""
353+
354+ # Any policy will work here, so we'll just pick one.
355+ POLICY = TeamSubscriptionPolicy.MODERATED
356+
357 def test___getTeam_with_team(self):
358 # _getTeam returns the context team for team updates.
359- self.setUpTeams(TeamSubscriptionPolicy.MODERATED)
360+ self.setUpTeams()
361 self.assertEqual(self.team, self.field._getTeam())
362
363 def test___getTeam_with_person_set(self):
364@@ -245,11 +256,17 @@
365 field = ITeamPublic['subscriptionpolicy'].bind(person_set)
366 self.assertEqual(None, field._getTeam())
367
368+
369+class TestTeamSubscriptionPolicyChoiceModerated(TeamSubscriptionPolicyBase):
370+ """Test `TeamSubsciptionPolicyChoice` Moderated constraints."""
371+
372+ POLICY = TeamSubscriptionPolicy.MODERATED
373+
374 def test_closed_team_with_closed_super_team_cannot_become_open(self):
375 # The team cannot compromise the membership of the super team
376 # by becoming open. The user must remove his team from the super team
377 # first.
378- self.setUpTeams(TeamSubscriptionPolicy.MODERATED)
379+ self.setUpTeams()
380 self.other_team.addMember(self.team, self.team.teamowner)
381 self.assertFalse(
382 self.field.constraint(TeamSubscriptionPolicy.OPEN))
383@@ -259,29 +276,16 @@
384
385 def test_closed_team_with_open_super_team_can_become_open(self):
386 # The team can become open if its super teams are open.
387- self.setUpTeams(
388- TeamSubscriptionPolicy.MODERATED, TeamSubscriptionPolicy.OPEN)
389+ self.setUpTeams(other_policy=TeamSubscriptionPolicy.OPEN)
390 self.other_team.addMember(self.team, self.team.teamowner)
391 self.assertTrue(
392 self.field.constraint(TeamSubscriptionPolicy.OPEN))
393 self.assertEqual(
394 None, self.field.validate(TeamSubscriptionPolicy.OPEN))
395
396- def test_open_team_with_open_sub_team_cannot_become_closed(self):
397- # The team cannot become closed if its membership will be
398- # compromised by an open subteam. The user must remove the subteam
399- # first
400- self.setUpTeams(TeamSubscriptionPolicy.OPEN)
401- self.team.addMember(self.other_team, self.team.teamowner)
402- self.assertFalse(
403- self.field.constraint(TeamSubscriptionPolicy.MODERATED))
404- self.assertRaises(
405- TeamSubscriptionPolicyError, self.field.validate,
406- TeamSubscriptionPolicy.MODERATED)
407-
408 def test_closed_team_can_change_to_another_closed_policy(self):
409 # A closed team can change between the two closed polcies.
410- self.setUpTeams(TeamSubscriptionPolicy.MODERATED)
411+ self.setUpTeams()
412 self.team.addMember(self.other_team, self.team.teamowner)
413 super_team = self.factory.makeTeam(
414 subscription_policy=TeamSubscriptionPolicy.MODERATED,
415@@ -292,20 +296,10 @@
416 self.assertEqual(
417 None, self.field.validate(TeamSubscriptionPolicy.RESTRICTED))
418
419- def test_open_team_with_closed_sub_team_can_become_closed(self):
420- # The team can become closed.
421- self.setUpTeams(
422- TeamSubscriptionPolicy.OPEN, TeamSubscriptionPolicy.MODERATED)
423- self.team.addMember(self.other_team, self.team.teamowner)
424- self.assertTrue(
425- self.field.constraint(TeamSubscriptionPolicy.MODERATED))
426- self.assertEqual(
427- None, self.field.validate(TeamSubscriptionPolicy.MODERATED))
428-
429 def test_closed_team_with_active_ppas_cannot_become_open(self):
430 # The team cannot become open if it has PPA because it compromises the
431 # the control of who can upload.
432- self.setUpTeams(TeamSubscriptionPolicy.MODERATED)
433+ self.setUpTeams()
434 self.team.createPPA()
435 self.assertFalse(
436 self.field.constraint(TeamSubscriptionPolicy.OPEN))
437@@ -315,7 +309,7 @@
438
439 def test_closed_team_without_active_ppas_can_become_open(self):
440 # The team can become if it has deleted PPAs.
441- self.setUpTeams(TeamSubscriptionPolicy.MODERATED)
442+ self.setUpTeams(other_policy=TeamSubscriptionPolicy.MODERATED)
443 ppa = self.team.createPPA()
444 ppa.delete(self.team.teamowner)
445 removeSecurityProxy(ppa).status = ArchiveStatus.DELETED
446@@ -325,6 +319,47 @@
447 None, self.field.validate(TeamSubscriptionPolicy.OPEN))
448
449
450+class TestTeamSubscriptionPolicyChoiceRestrcted(
451+ TestTeamSubscriptionPolicyChoiceModerated):
452+ """Test `TeamSubsciptionPolicyChoice` Restricted constraints."""
453+
454+ POLICY = TeamSubscriptionPolicy.RESTRICTED
455+
456+
457+class TestTeamSubscriptionPolicyChoiceOpen(TeamSubscriptionPolicyBase):
458+ """Test `TeamSubsciptionPolicyChoice` Open constraints."""
459+
460+ POLICY = TeamSubscriptionPolicy.OPEN
461+
462+ def test_open_team_with_open_sub_team_cannot_become_closed(self):
463+ # The team cannot become closed if its membership will be
464+ # compromised by an open subteam. The user must remove the subteam
465+ # first
466+ self.setUpTeams()
467+ self.team.addMember(self.other_team, self.team.teamowner)
468+ self.assertFalse(
469+ self.field.constraint(TeamSubscriptionPolicy.MODERATED))
470+ self.assertRaises(
471+ TeamSubscriptionPolicyError, self.field.validate,
472+ TeamSubscriptionPolicy.MODERATED)
473+
474+ def test_open_team_with_closed_sub_team_can_become_closed(self):
475+ # The team can become closed.
476+ self.setUpTeams(other_policy=TeamSubscriptionPolicy.MODERATED)
477+ self.team.addMember(self.other_team, self.team.teamowner)
478+ self.assertTrue(
479+ self.field.constraint(TeamSubscriptionPolicy.MODERATED))
480+ self.assertEqual(
481+ None, self.field.validate(TeamSubscriptionPolicy.MODERATED))
482+
483+
484+class TestTeamSubscriptionPolicyChoiceDelegated(
485+ TestTeamSubscriptionPolicyChoiceOpen):
486+ """Test `TeamSubsciptionPolicyChoice` Delegated constraints."""
487+
488+ POLICY = TeamSubscriptionPolicy.DELEGATED
489+
490+
491 class TestVisibilityConsistencyWarning(TestCaseWithFactory):
492
493 layer = DatabaseFunctionalLayer
494@@ -346,6 +381,52 @@
495 self.team.visibilityConsistencyWarning(PersonVisibility.PRIVATE))
496
497
498+class TestPersonJoinTeam(TestCaseWithFactory):
499+
500+ layer = DatabaseFunctionalLayer
501+
502+ def test_join_restricted_team_error(self):
503+ # Calling join with a Restricted team raises an error.
504+ team = self.factory.makeTeam(
505+ subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
506+ user = self.factory.makePerson()
507+ login_person(user)
508+ self.assertRaises(JoinNotAllowed, user.join, team, user)
509+
510+ def test_join_moderated_team_proposed(self):
511+ # Joining a Moderated team creates a Proposed TeamMembership.
512+ team = self.factory.makeTeam(
513+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
514+ user = self.factory.makePerson()
515+ login_person(user)
516+ user.join(team, user)
517+ users = list(team.proposedmembers)
518+ self.assertEqual(1, len(users))
519+ self.assertEqual(user, users[0])
520+
521+ def test_join_delegated_team_proposed(self):
522+ # Joining a Delegated team creates a Proposed TeamMembership.
523+ team = self.factory.makeTeam(
524+ subscription_policy=TeamSubscriptionPolicy.DELEGATED)
525+ user = self.factory.makePerson()
526+ login_person(user)
527+ user.join(team, user)
528+ users = list(team.proposedmembers)
529+ self.assertEqual(1, len(users))
530+ self.assertEqual(user, users[0])
531+
532+ def test_join_open_team_appoved(self):
533+ # Joining an Open team creates an Approved TeamMembership.
534+ team = self.factory.makeTeam(
535+ subscription_policy=TeamSubscriptionPolicy.OPEN)
536+ user = self.factory.makePerson()
537+ login_person(user)
538+ user.join(team, user)
539+ members = list(team.approvedmembers)
540+ self.assertEqual(1, len(members))
541+ self.assertEqual(user, members[0])
542+
543+
544 class TestMembershipManagement(TestCaseWithFactory):
545
546 layer = DatabaseFunctionalLayer
547
548=== modified file 'lib/lp/registry/vocabularies.py'
549--- lib/lp/registry/vocabularies.py 2010-12-22 02:48:42 +0000
550+++ lib/lp/registry/vocabularies.py 2011-02-03 21:26:00 +0000
551@@ -140,11 +140,11 @@
552 IProjectGroupMilestone,
553 )
554 from lp.registry.interfaces.person import (
555+ CLOSED_TEAM_POLICY,
556 IPerson,
557 IPersonSet,
558 ITeam,
559 PersonVisibility,
560- TeamSubscriptionPolicy,
561 )
562 from lp.registry.interfaces.pillar import IPillarName
563 from lp.registry.interfaces.product import (
564@@ -737,7 +737,7 @@
565
566 @property
567 def is_closed_team(self):
568- return self.team.subscriptionpolicy != TeamSubscriptionPolicy.OPEN
569+ return self.team.subscriptionpolicy in CLOSED_TEAM_POLICY
570
571 @property
572 def step_title(self):
573@@ -781,7 +781,7 @@
574 if self.is_closed_team:
575 clause = And(
576 clause,
577- Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)
578+ Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY))
579 return clause
580
581
582@@ -819,7 +819,7 @@
583 if self.is_closed_team:
584 clause = And(
585 clause,
586- Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)
587+ Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY))
588 return clause
589
590
591
592=== modified file 'lib/lp/soyuz/doc/archive.txt'
593--- lib/lp/soyuz/doc/archive.txt 2011-01-03 22:15:46 +0000
594+++ lib/lp/soyuz/doc/archive.txt 2011-02-03 21:26:00 +0000
595@@ -1971,7 +1971,7 @@
596 >>> from lp.registry.interfaces.person import (
597 ... TeamSubscriptionPolicy)
598 >>> team = getUtility(IPersonSet).newTeam(mark, 't1', 't1',
599- ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN)
600+ ... subscriptionpolicy=TeamSubscriptionPolicy.MODERATED)
601 >>> copy = factory.makeCopyArchiveLocation(distribution=ubuntu,
602 ... name="team-archive",
603 ... owner=team)
604@@ -2070,7 +2070,7 @@
605 And if the archive is owned by a team, then anyone in the team will also
606 be able to view the private team archive:
607
608- >>> cprov.join(team)
609+ >>> ignore = team.addMember(cprov, team.teamowner)
610 >>> ubuntu_copy_archives = archive_set.getArchivesForDistribution(
611 ... ubuntu, purposes=[ArchivePurpose.COPY], user=cprov)
612 >>> print_archive_names(ubuntu_copy_archives)
613
614=== modified file 'lib/lp/soyuz/model/archive.py'
615--- lib/lp/soyuz/model/archive.py 2011-01-20 18:27:00 +0000
616+++ lib/lp/soyuz/model/archive.py 2011-02-03 21:26:00 +0000
617@@ -80,8 +80,8 @@
618 from lp.buildmaster.model.packagebuild import PackageBuild
619 from lp.registry.interfaces.distroseries import IDistroSeriesSet
620 from lp.registry.interfaces.person import (
621+ OPEN_TEAM_POLICY,
622 PersonVisibility,
623- TeamSubscriptionPolicy,
624 validate_person,
625 )
626 from lp.registry.interfaces.pocket import PackagePublishingPocket
627@@ -1704,7 +1704,7 @@
628 def validatePPA(self, person, proposed_name):
629 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
630 if person.isTeam() and (
631- person.subscriptionpolicy == TeamSubscriptionPolicy.OPEN):
632+ person.subscriptionpolicy in OPEN_TEAM_POLICY):
633 return "Open teams cannot have PPAs."
634 if proposed_name is not None and proposed_name == ubuntu.name:
635 return (

Subscribers

People subscribed via source and target branches

to status/vote changes: