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
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-01 06:02:46 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-02-03 21:26:00 +0000
@@ -813,6 +813,7 @@
813 margin-left: 2.6em;813 margin-left: 2.6em;
814 }814 }
815.formHelp {815.formHelp {
816 max-width: 45em;
816 margin: 0.2em 0 0.5em 0.2em;817 margin: 0.2em 0 0.5em 0.2em;
817 color: #777;818 color: #777;
818 }819 }
819820
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2011-02-02 15:42:48 +0000
+++ lib/lp/registry/browser/person.py 2011-02-03 21:26:00 +0000
@@ -3009,27 +3009,6 @@
3009 profile_url.host = config.launchpad.non_restricted_hostname3009 profile_url.host = config.launchpad.non_restricted_hostname
3010 return str(profile_url)3010 return str(profile_url)
30113011
3012 @property
3013 def subscription_policy_description(self):
3014 """Return the description of this team's subscription policy."""
3015 assert self.team.isTeam(), (
3016 'This method can only be called when the context is a team.')
3017 if self.team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
3018 description = _(
3019 "This is a restricted team; new members can only be added "
3020 "by one of the team's administrators.")
3021 elif self.team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
3022 description = _(
3023 "This is a moderated team; all subscriptions are subjected "
3024 "to approval by one of the team's administrators.")
3025 elif self.team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
3026 description = _(
3027 "This is an open team; any user can join and no approval "
3028 "is required.")
3029 else:
3030 raise AssertionError('Unknown subscription policy.')
3031 return description
3032
3033 def getURLToAssignedBugsInProgress(self):3012 def getURLToAssignedBugsInProgress(self):
3034 """Return an URL to a page which lists all bugs assigned to this3013 """Return an URL to a page which lists all bugs assigned to this
3035 person that are In Progress.3014 person that are In Progress.
30363015
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2011-02-02 15:43:31 +0000
+++ lib/lp/registry/browser/team.py 2011-02-03 21:26:00 +0000
@@ -65,7 +65,10 @@
65 )65 )
66from lp.app.browser.tales import PersonFormatterAPI66from lp.app.browser.tales import PersonFormatterAPI
67from lp.app.errors import UnexpectedFormData67from lp.app.errors import UnexpectedFormData
68from lp.app.widgets.itemswidgets import LaunchpadRadioWidget68from lp.app.widgets.itemswidgets import (
69 LaunchpadRadioWidget,
70 LaunchpadRadioWidgetWithDescription,
71 )
69from lp.app.widgets.owner import HiddenUserWidget72from lp.app.widgets.owner import HiddenUserWidget
70from lp.registry.browser.branding import BrandingChangeView73from lp.registry.browser.branding import BrandingChangeView
71from lp.registry.interfaces.mailinglist import (74from lp.registry.interfaces.mailinglist import (
@@ -209,7 +212,8 @@
209 custom_widget(212 custom_widget(
210 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')213 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')
211 custom_widget(214 custom_widget(
212 'subscriptionpolicy', LaunchpadRadioWidget, orientation='vertical')215 'subscriptionpolicy', LaunchpadRadioWidgetWithDescription,
216 orientation='vertical')
213 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)217 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
214218
215 def setUpFields(self):219 def setUpFields(self):
@@ -889,7 +893,8 @@
889 custom_widget(893 custom_widget(
890 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')894 'renewal_policy', LaunchpadRadioWidget, orientation='vertical')
891 custom_widget(895 custom_widget(
892 'subscriptionpolicy', LaunchpadRadioWidget, orientation='vertical')896 'subscriptionpolicy', LaunchpadRadioWidgetWithDescription,
897 orientation='vertical')
893 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)898 custom_widget('teamdescription', TextAreaWidget, height=10, width=30)
894899
895 def setUpFields(self):900 def setUpFields(self):
896901
=== added file 'lib/lp/registry/help/team-subscription-policy.html'
--- lib/lp/registry/help/team-subscription-policy.html 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/help/team-subscription-policy.html 2011-02-03 21:26:00 +0000
@@ -0,0 +1,59 @@
1<html>
2 <head>
3 <title>Team subscription policy</title>
4 <link rel="stylesheet" type="text/css"
5 href="/+icing/yui/cssreset/reset.css" />
6 <link rel="stylesheet" type="text/css"
7 href="/+icing/yui/cssfonts/fonts.css" />
8 <link rel="stylesheet" type="text/css"
9 href="/+icing/yui/cssbase/base.css" />
10 </head>
11 <body>
12 <h1>Team subscription policy</h1>
13
14 <p>
15 There are four kinds of policy that describe who can be a member and
16 how new memberships are handled. The choice of policy reflects the need
17 to build a community versus the need to control Launchpad assets.
18 </p>
19
20 <dl>
21 <dt>Open</dt>
22 <dd>
23 Membership is open, no approval required, and subteams can be open or
24 closed. Any user can be a member of the team and no approval is
25 required. Subteams can be Open, Delegated, Moderated, or Restricted.
26 Open is a good choice for encouraging a community of contributors.
27 Open teams cannot have PPAs.
28 </dd>
29
30 <dt>Delegated</dt>
31 <dd>
32 Membership is open, requires approval, and subteams can be open or
33 closed. Any user can be a member of the team via a subteam, but team
34 administrators approve direct memberships. Subteams can be Open,
35 Delegated, Moderated, or Restricted. Delegated is a good choice for
36 managing a large community of contributors. Delegated teams cannot
37 have PPAs.
38 </dd>
39
40 <dt>Moderated</dt>
41 <dd>
42 Membership is closed, requires approval, and subteams must be closed.
43 Any user can propose a new member, but team administrators approve
44 membership. Subteams must be Moderated or Restricted. Moderated is a
45 good choice for teams that manage things that need to be secure, like
46 projects, branches, or PPAs, but want to encourage users to help.
47 </dd>
48
49 <dt>Restricted</dt>
50 <dd>
51 Membership is closed, requires approval, and subteams must be closed.
52 Only the team's administrators can invite a user to be a member.
53 Subteams must be Moderated or Restricted. Restricted is a good choice
54 for teams that manage things that need to be secure, like projects,
55 branches, or PPAs.
56 </dd>
57 </dl>
58 </body>
59</html>
060
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2011-01-10 14:55:31 +0000
+++ lib/lp/registry/interfaces/person.py 2011-02-03 21:26:00 +0000
@@ -8,6 +8,7 @@
8__metaclass__ = type8__metaclass__ = type
99
10__all__ = [10__all__ = [
11 'CLOSED_TEAM_POLICY',
11 'IAdminPeopleMergeSchema',12 'IAdminPeopleMergeSchema',
12 'IAdminTeamMergeSchema',13 'IAdminTeamMergeSchema',
13 'IHasStanding',14 'IHasStanding',
@@ -27,6 +28,7 @@
27 'ImmutableVisibilityError',28 'ImmutableVisibilityError',
28 'InvalidName',29 'InvalidName',
29 'NoSuchPerson',30 'NoSuchPerson',
31 'OPEN_TEAM_POLICY',
30 'PersonCreationRationale',32 'PersonCreationRationale',
31 'PersonVisibility',33 'PersonVisibility',
32 'PersonalStanding',34 'PersonalStanding',
@@ -392,31 +394,61 @@
392class TeamSubscriptionPolicy(DBEnumeratedType):394class TeamSubscriptionPolicy(DBEnumeratedType):
393 """Team Subscription Policies395 """Team Subscription Policies
394396
395 The policies that apply to a team and specify how new subscriptions must397 The policies that describe who can be a member and how new memberships
396 be handled. More information can be found in the TeamMembershipPolicies398 are handled. The choice of policy reflects the need to build a community
397 spec.399 versus the need to control Launchpad assets.
398 """400 """
399401
402 OPEN = DBItem(2, """
403 Open Team
404
405 Membership is open, no approval required, and subteams can be open or
406 closed. Any user can be a member of the team and no approval is
407 required. Subteams can be Open, Delegated, Moderated, or Restricted.
408 Open is a good choice for encouraging a community of contributors.
409 Open teams cannot have PPAs.
410 """)
411
412 DELEGATED = DBItem(4, """
413 Delegated Team
414
415 Membership is open, requires approval, and subteams can be open or
416 closed. Any user can be a member of the team via a subteam, but team
417 administrators approve direct memberships. Subteams can be Open,
418 Delegated, Moderated, or Restricted. Delegated is a good choice for
419 managing a large community of contributors. Delegated teams cannot
420 have PPAs.
421 """)
422
400 MODERATED = DBItem(1, """423 MODERATED = DBItem(1, """
401 Moderated Team424 Moderated Team
402425
403 All subscriptions for this team are subject to approval by one of426 Membership is closed, requires approval, and subteams must be closed.
404 the team's administrators.427 Any user can propose a new member, but team administrators approve
405 """)428 membership. Subteams must be Moderated or Restricted. Moderated is a
406429 good choice for teams that manage things that need to be secure, like
407 OPEN = DBItem(2, """430 projects, branches, or PPAs, but want to encourage users to help.
408 Open Team
409
410 Any user can join and no approval is required.
411 """)431 """)
412432
413 RESTRICTED = DBItem(3, """433 RESTRICTED = DBItem(3, """
414 Restricted Team434 Restricted Team
415435
416 New members can only be added by one of the team's administrators.436 Membership is closed, requires approval, and subteams must be closed.
437 Only the team's administrators can invite a user to be a member.
438 Subteams must be Moderated or Restricted. Restricted is a good choice
439 for teams that manage things that need to be secure, like projects,
440 branches, or PPAs.
417 """)441 """)
418442
419443
444OPEN_TEAM_POLICY = (
445 TeamSubscriptionPolicy.OPEN, TeamSubscriptionPolicy.DELEGATED)
446
447
448CLOSED_TEAM_POLICY = (
449 TeamSubscriptionPolicy.RESTRICTED, TeamSubscriptionPolicy.MODERATED)
450
451
420class PersonVisibility(DBEnumeratedType):452class PersonVisibility(DBEnumeratedType):
421 """The visibility level of person or team objects.453 """The visibility level of person or team objects.
422454
@@ -503,10 +535,10 @@
503 if team is None or policy == team.subscriptionpolicy:535 if team is None or policy == team.subscriptionpolicy:
504 # The team is being initialized or the policy is not changing.536 # The team is being initialized or the policy is not changing.
505 return True537 return True
506 elif policy == TeamSubscriptionPolicy.OPEN:538 elif policy in OPEN_TEAM_POLICY:
507 # The team can be open if its super teams are open.539 # The team can be open if its super teams are open.
508 for team in team.super_teams:540 for team in team.super_teams:
509 if team.subscriptionpolicy != TeamSubscriptionPolicy.OPEN:541 if team.subscriptionpolicy in CLOSED_TEAM_POLICY:
510 raise TeamSubscriptionPolicyError(542 raise TeamSubscriptionPolicyError(
511 "The team subscription policy cannot be %s because one "543 "The team subscription policy cannot be %s because one "
512 "or more if its super teams are not open." % policy)544 "or more if its super teams are not open." % policy)
@@ -516,11 +548,11 @@
516 raise TeamSubscriptionPolicyError(548 raise TeamSubscriptionPolicyError(
517 "The team subscription policy cannot be %s because it "549 "The team subscription policy cannot be %s because it "
518 "has one or more active PPAs." % policy)550 "has one or more active PPAs." % policy)
519 elif team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:551 elif team.subscriptionpolicy in OPEN_TEAM_POLICY:
520 # The team can become MODERATED or RESTRICTED if its member teams552 # The team can become MODERATED or RESTRICTED if its member teams
521 # are not OPEN.553 # are not OPEN.
522 for member in team.activemembers:554 for member in team.activemembers:
523 if member.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:555 if member.subscriptionpolicy in OPEN_TEAM_POLICY:
524 raise TeamSubscriptionPolicyError(556 raise TeamSubscriptionPolicyError(
525 "The team subscription policy cannot be %s because one "557 "The team subscription policy cannot be %s because one "
526 "or more if its member teams are Open." % policy)558 "or more if its member teams are Open." % policy)
@@ -1770,10 +1802,7 @@
1770 vocabulary=TeamSubscriptionPolicy,1802 vocabulary=TeamSubscriptionPolicy,
1771 default=TeamSubscriptionPolicy.MODERATED, required=True,1803 default=TeamSubscriptionPolicy.MODERATED, required=True,
1772 description=_(1804 description=_(
1773 "'Moderated' means all subscriptions must be approved. "1805 TeamSubscriptionPolicy.__doc__.split('\n\n')[1])),
1774 "'Open' means any user can join without approval. "
1775 "'Restricted' means new members can be added only by a "
1776 "team administrator.")),
1777 exported_as='subscription_policy')1806 exported_as='subscription_policy')
17781807
1779 renewal_policy = exported(1808 renewal_policy = exported(
17801809
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2011-02-03 08:38:27 +0000
+++ lib/lp/registry/model/person.py 2011-02-03 21:26:00 +0000
@@ -1273,7 +1273,8 @@
12731273
1274 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:1274 if team.subscriptionpolicy == TeamSubscriptionPolicy.RESTRICTED:
1275 raise JoinNotAllowed("This is a restricted team")1275 raise JoinNotAllowed("This is a restricted team")
1276 elif team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:1276 elif (team.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED
1277 or team.subscriptionpolicy == TeamSubscriptionPolicy.DELEGATED):
1277 status = proposed1278 status = proposed
1278 elif team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:1279 elif team.subscriptionpolicy == TeamSubscriptionPolicy.OPEN:
1279 status = approved1280 status = approved
12801281
=== modified file 'lib/lp/registry/templates/person-macros.pt'
--- lib/lp/registry/templates/person-macros.pt 2010-05-18 07:44:47 +0000
+++ lib/lp/registry/templates/person-macros.pt 2011-02-03 21:26:00 +0000
@@ -61,10 +61,11 @@
61 </dl>61 </dl>
62 <dl id="subscription-policy">62 <dl id="subscription-policy">
63 <dt>Subscription policy:</dt>63 <dt>Subscription policy:</dt>
64 <dd>64 <dd
65 tal:attributes="title context/subscriptionpolicy/description">
65 <span tal:replace="context/subscriptionpolicy/title" />66 <span tal:replace="context/subscriptionpolicy/title" />
66 <img src="/@@/maybe"67 <a class="sprite maybe" href="/+help/team-subscription-policy.html"
67 tal:attributes="title context/subscriptionpolicy/description" />68 target="help">&nbsp;</a>
68 </dd>69 </dd>
69 </dl>70 </dl>
7071
7172
=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 2010-12-13 18:08:19 +0000
+++ lib/lp/registry/tests/test_team.py 2011-02-03 21:26:00 +0000
@@ -18,7 +18,10 @@
18 FunctionalLayer,18 FunctionalLayer,
19 )19 )
20from lp.registry.enum import PersonTransferJobType20from lp.registry.enum import PersonTransferJobType
21from lp.registry.errors import TeamSubscriptionPolicyError21from lp.registry.errors import (
22 JoinNotAllowed,
23 TeamSubscriptionPolicyError,
24 )
22from lp.registry.interfaces.mailinglist import MailingListStatus25from lp.registry.interfaces.mailinglist import MailingListStatus
23from lp.registry.interfaces.person import (26from lp.registry.interfaces.person import (
24 IPersonSet,27 IPersonSet,
@@ -220,23 +223,31 @@
220 self.assertEqual('a message', error.doc())223 self.assertEqual('a message', error.doc())
221224
222225
223class TestTeamSubscriptionPolicyChoice(TestCaseWithFactory):226class TeamSubscriptionPolicyBase(TestCaseWithFactory):
224 """Test `TeamSubsciptionPolicyChoice` constraints."""227 """`TeamSubsciptionPolicyChoice` base test class."""
225228
226 layer = DatabaseFunctionalLayer229 layer = DatabaseFunctionalLayer
230 POLICY = None
227231
228 def setUpTeams(self, policy, other_policy=None):232 def setUpTeams(self, other_policy=None):
229 if other_policy is None:233 if other_policy is None:
230 other_policy = policy234 other_policy = self.POLICY
231 self.team = self.factory.makeTeam(subscription_policy=policy)235 self.team = self.factory.makeTeam(subscription_policy=self.POLICY)
232 self.other_team = self.factory.makeTeam(236 self.other_team = self.factory.makeTeam(
233 subscription_policy=other_policy, owner=self.team.teamowner)237 subscription_policy=other_policy, owner=self.team.teamowner)
234 self.field = ITeamPublic['subscriptionpolicy'].bind(self.team)238 self.field = ITeamPublic['subscriptionpolicy'].bind(self.team)
235 login_person(self.team.teamowner)239 login_person(self.team.teamowner)
236240
241
242class TestTeamSubscriptionPolicyChoiceCommon(TeamSubscriptionPolicyBase):
243 """Test `TeamSubsciptionPolicyChoice` constraints."""
244
245 # Any policy will work here, so we'll just pick one.
246 POLICY = TeamSubscriptionPolicy.MODERATED
247
237 def test___getTeam_with_team(self):248 def test___getTeam_with_team(self):
238 # _getTeam returns the context team for team updates.249 # _getTeam returns the context team for team updates.
239 self.setUpTeams(TeamSubscriptionPolicy.MODERATED)250 self.setUpTeams()
240 self.assertEqual(self.team, self.field._getTeam())251 self.assertEqual(self.team, self.field._getTeam())
241252
242 def test___getTeam_with_person_set(self):253 def test___getTeam_with_person_set(self):
@@ -245,11 +256,17 @@
245 field = ITeamPublic['subscriptionpolicy'].bind(person_set)256 field = ITeamPublic['subscriptionpolicy'].bind(person_set)
246 self.assertEqual(None, field._getTeam())257 self.assertEqual(None, field._getTeam())
247258
259
260class TestTeamSubscriptionPolicyChoiceModerated(TeamSubscriptionPolicyBase):
261 """Test `TeamSubsciptionPolicyChoice` Moderated constraints."""
262
263 POLICY = TeamSubscriptionPolicy.MODERATED
264
248 def test_closed_team_with_closed_super_team_cannot_become_open(self):265 def test_closed_team_with_closed_super_team_cannot_become_open(self):
249 # The team cannot compromise the membership of the super team266 # The team cannot compromise the membership of the super team
250 # by becoming open. The user must remove his team from the super team267 # by becoming open. The user must remove his team from the super team
251 # first.268 # first.
252 self.setUpTeams(TeamSubscriptionPolicy.MODERATED)269 self.setUpTeams()
253 self.other_team.addMember(self.team, self.team.teamowner)270 self.other_team.addMember(self.team, self.team.teamowner)
254 self.assertFalse(271 self.assertFalse(
255 self.field.constraint(TeamSubscriptionPolicy.OPEN))272 self.field.constraint(TeamSubscriptionPolicy.OPEN))
@@ -259,29 +276,16 @@
259276
260 def test_closed_team_with_open_super_team_can_become_open(self):277 def test_closed_team_with_open_super_team_can_become_open(self):
261 # The team can become open if its super teams are open.278 # The team can become open if its super teams are open.
262 self.setUpTeams(279 self.setUpTeams(other_policy=TeamSubscriptionPolicy.OPEN)
263 TeamSubscriptionPolicy.MODERATED, TeamSubscriptionPolicy.OPEN)
264 self.other_team.addMember(self.team, self.team.teamowner)280 self.other_team.addMember(self.team, self.team.teamowner)
265 self.assertTrue(281 self.assertTrue(
266 self.field.constraint(TeamSubscriptionPolicy.OPEN))282 self.field.constraint(TeamSubscriptionPolicy.OPEN))
267 self.assertEqual(283 self.assertEqual(
268 None, self.field.validate(TeamSubscriptionPolicy.OPEN))284 None, self.field.validate(TeamSubscriptionPolicy.OPEN))
269285
270 def test_open_team_with_open_sub_team_cannot_become_closed(self):
271 # The team cannot become closed if its membership will be
272 # compromised by an open subteam. The user must remove the subteam
273 # first
274 self.setUpTeams(TeamSubscriptionPolicy.OPEN)
275 self.team.addMember(self.other_team, self.team.teamowner)
276 self.assertFalse(
277 self.field.constraint(TeamSubscriptionPolicy.MODERATED))
278 self.assertRaises(
279 TeamSubscriptionPolicyError, self.field.validate,
280 TeamSubscriptionPolicy.MODERATED)
281
282 def test_closed_team_can_change_to_another_closed_policy(self):286 def test_closed_team_can_change_to_another_closed_policy(self):
283 # A closed team can change between the two closed polcies.287 # A closed team can change between the two closed polcies.
284 self.setUpTeams(TeamSubscriptionPolicy.MODERATED)288 self.setUpTeams()
285 self.team.addMember(self.other_team, self.team.teamowner)289 self.team.addMember(self.other_team, self.team.teamowner)
286 super_team = self.factory.makeTeam(290 super_team = self.factory.makeTeam(
287 subscription_policy=TeamSubscriptionPolicy.MODERATED,291 subscription_policy=TeamSubscriptionPolicy.MODERATED,
@@ -292,20 +296,10 @@
292 self.assertEqual(296 self.assertEqual(
293 None, self.field.validate(TeamSubscriptionPolicy.RESTRICTED))297 None, self.field.validate(TeamSubscriptionPolicy.RESTRICTED))
294298
295 def test_open_team_with_closed_sub_team_can_become_closed(self):
296 # The team can become closed.
297 self.setUpTeams(
298 TeamSubscriptionPolicy.OPEN, TeamSubscriptionPolicy.MODERATED)
299 self.team.addMember(self.other_team, self.team.teamowner)
300 self.assertTrue(
301 self.field.constraint(TeamSubscriptionPolicy.MODERATED))
302 self.assertEqual(
303 None, self.field.validate(TeamSubscriptionPolicy.MODERATED))
304
305 def test_closed_team_with_active_ppas_cannot_become_open(self):299 def test_closed_team_with_active_ppas_cannot_become_open(self):
306 # The team cannot become open if it has PPA because it compromises the300 # The team cannot become open if it has PPA because it compromises the
307 # the control of who can upload.301 # the control of who can upload.
308 self.setUpTeams(TeamSubscriptionPolicy.MODERATED)302 self.setUpTeams()
309 self.team.createPPA()303 self.team.createPPA()
310 self.assertFalse(304 self.assertFalse(
311 self.field.constraint(TeamSubscriptionPolicy.OPEN))305 self.field.constraint(TeamSubscriptionPolicy.OPEN))
@@ -315,7 +309,7 @@
315309
316 def test_closed_team_without_active_ppas_can_become_open(self):310 def test_closed_team_without_active_ppas_can_become_open(self):
317 # The team can become if it has deleted PPAs.311 # The team can become if it has deleted PPAs.
318 self.setUpTeams(TeamSubscriptionPolicy.MODERATED)312 self.setUpTeams(other_policy=TeamSubscriptionPolicy.MODERATED)
319 ppa = self.team.createPPA()313 ppa = self.team.createPPA()
320 ppa.delete(self.team.teamowner)314 ppa.delete(self.team.teamowner)
321 removeSecurityProxy(ppa).status = ArchiveStatus.DELETED315 removeSecurityProxy(ppa).status = ArchiveStatus.DELETED
@@ -325,6 +319,47 @@
325 None, self.field.validate(TeamSubscriptionPolicy.OPEN))319 None, self.field.validate(TeamSubscriptionPolicy.OPEN))
326320
327321
322class TestTeamSubscriptionPolicyChoiceRestrcted(
323 TestTeamSubscriptionPolicyChoiceModerated):
324 """Test `TeamSubsciptionPolicyChoice` Restricted constraints."""
325
326 POLICY = TeamSubscriptionPolicy.RESTRICTED
327
328
329class TestTeamSubscriptionPolicyChoiceOpen(TeamSubscriptionPolicyBase):
330 """Test `TeamSubsciptionPolicyChoice` Open constraints."""
331
332 POLICY = TeamSubscriptionPolicy.OPEN
333
334 def test_open_team_with_open_sub_team_cannot_become_closed(self):
335 # The team cannot become closed if its membership will be
336 # compromised by an open subteam. The user must remove the subteam
337 # first
338 self.setUpTeams()
339 self.team.addMember(self.other_team, self.team.teamowner)
340 self.assertFalse(
341 self.field.constraint(TeamSubscriptionPolicy.MODERATED))
342 self.assertRaises(
343 TeamSubscriptionPolicyError, self.field.validate,
344 TeamSubscriptionPolicy.MODERATED)
345
346 def test_open_team_with_closed_sub_team_can_become_closed(self):
347 # The team can become closed.
348 self.setUpTeams(other_policy=TeamSubscriptionPolicy.MODERATED)
349 self.team.addMember(self.other_team, self.team.teamowner)
350 self.assertTrue(
351 self.field.constraint(TeamSubscriptionPolicy.MODERATED))
352 self.assertEqual(
353 None, self.field.validate(TeamSubscriptionPolicy.MODERATED))
354
355
356class TestTeamSubscriptionPolicyChoiceDelegated(
357 TestTeamSubscriptionPolicyChoiceOpen):
358 """Test `TeamSubsciptionPolicyChoice` Delegated constraints."""
359
360 POLICY = TeamSubscriptionPolicy.DELEGATED
361
362
328class TestVisibilityConsistencyWarning(TestCaseWithFactory):363class TestVisibilityConsistencyWarning(TestCaseWithFactory):
329364
330 layer = DatabaseFunctionalLayer365 layer = DatabaseFunctionalLayer
@@ -346,6 +381,52 @@
346 self.team.visibilityConsistencyWarning(PersonVisibility.PRIVATE))381 self.team.visibilityConsistencyWarning(PersonVisibility.PRIVATE))
347382
348383
384class TestPersonJoinTeam(TestCaseWithFactory):
385
386 layer = DatabaseFunctionalLayer
387
388 def test_join_restricted_team_error(self):
389 # Calling join with a Restricted team raises an error.
390 team = self.factory.makeTeam(
391 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
392 user = self.factory.makePerson()
393 login_person(user)
394 self.assertRaises(JoinNotAllowed, user.join, team, user)
395
396 def test_join_moderated_team_proposed(self):
397 # Joining a Moderated team creates a Proposed TeamMembership.
398 team = self.factory.makeTeam(
399 subscription_policy=TeamSubscriptionPolicy.MODERATED)
400 user = self.factory.makePerson()
401 login_person(user)
402 user.join(team, user)
403 users = list(team.proposedmembers)
404 self.assertEqual(1, len(users))
405 self.assertEqual(user, users[0])
406
407 def test_join_delegated_team_proposed(self):
408 # Joining a Delegated team creates a Proposed TeamMembership.
409 team = self.factory.makeTeam(
410 subscription_policy=TeamSubscriptionPolicy.DELEGATED)
411 user = self.factory.makePerson()
412 login_person(user)
413 user.join(team, user)
414 users = list(team.proposedmembers)
415 self.assertEqual(1, len(users))
416 self.assertEqual(user, users[0])
417
418 def test_join_open_team_appoved(self):
419 # Joining an Open team creates an Approved TeamMembership.
420 team = self.factory.makeTeam(
421 subscription_policy=TeamSubscriptionPolicy.OPEN)
422 user = self.factory.makePerson()
423 login_person(user)
424 user.join(team, user)
425 members = list(team.approvedmembers)
426 self.assertEqual(1, len(members))
427 self.assertEqual(user, members[0])
428
429
349class TestMembershipManagement(TestCaseWithFactory):430class TestMembershipManagement(TestCaseWithFactory):
350431
351 layer = DatabaseFunctionalLayer432 layer = DatabaseFunctionalLayer
352433
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-12-22 02:48:42 +0000
+++ lib/lp/registry/vocabularies.py 2011-02-03 21:26:00 +0000
@@ -140,11 +140,11 @@
140 IProjectGroupMilestone,140 IProjectGroupMilestone,
141 )141 )
142from lp.registry.interfaces.person import (142from lp.registry.interfaces.person import (
143 CLOSED_TEAM_POLICY,
143 IPerson,144 IPerson,
144 IPersonSet,145 IPersonSet,
145 ITeam,146 ITeam,
146 PersonVisibility,147 PersonVisibility,
147 TeamSubscriptionPolicy,
148 )148 )
149from lp.registry.interfaces.pillar import IPillarName149from lp.registry.interfaces.pillar import IPillarName
150from lp.registry.interfaces.product import (150from lp.registry.interfaces.product import (
@@ -737,7 +737,7 @@
737737
738 @property738 @property
739 def is_closed_team(self):739 def is_closed_team(self):
740 return self.team.subscriptionpolicy != TeamSubscriptionPolicy.OPEN740 return self.team.subscriptionpolicy in CLOSED_TEAM_POLICY
741741
742 @property742 @property
743 def step_title(self):743 def step_title(self):
@@ -781,7 +781,7 @@
781 if self.is_closed_team:781 if self.is_closed_team:
782 clause = And(782 clause = And(
783 clause,783 clause,
784 Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)784 Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY))
785 return clause785 return clause
786786
787787
@@ -819,7 +819,7 @@
819 if self.is_closed_team:819 if self.is_closed_team:
820 clause = And(820 clause = And(
821 clause,821 clause,
822 Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)822 Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY))
823 return clause823 return clause
824824
825825
826826
=== modified file 'lib/lp/soyuz/doc/archive.txt'
--- lib/lp/soyuz/doc/archive.txt 2011-01-03 22:15:46 +0000
+++ lib/lp/soyuz/doc/archive.txt 2011-02-03 21:26:00 +0000
@@ -1971,7 +1971,7 @@
1971 >>> from lp.registry.interfaces.person import (1971 >>> from lp.registry.interfaces.person import (
1972 ... TeamSubscriptionPolicy)1972 ... TeamSubscriptionPolicy)
1973 >>> team = getUtility(IPersonSet).newTeam(mark, 't1', 't1',1973 >>> team = getUtility(IPersonSet).newTeam(mark, 't1', 't1',
1974 ... subscriptionpolicy=TeamSubscriptionPolicy.OPEN)1974 ... subscriptionpolicy=TeamSubscriptionPolicy.MODERATED)
1975 >>> copy = factory.makeCopyArchiveLocation(distribution=ubuntu,1975 >>> copy = factory.makeCopyArchiveLocation(distribution=ubuntu,
1976 ... name="team-archive",1976 ... name="team-archive",
1977 ... owner=team)1977 ... owner=team)
@@ -2070,7 +2070,7 @@
2070And if the archive is owned by a team, then anyone in the team will also2070And if the archive is owned by a team, then anyone in the team will also
2071be able to view the private team archive:2071be able to view the private team archive:
20722072
2073 >>> cprov.join(team)2073 >>> ignore = team.addMember(cprov, team.teamowner)
2074 >>> ubuntu_copy_archives = archive_set.getArchivesForDistribution(2074 >>> ubuntu_copy_archives = archive_set.getArchivesForDistribution(
2075 ... ubuntu, purposes=[ArchivePurpose.COPY], user=cprov)2075 ... ubuntu, purposes=[ArchivePurpose.COPY], user=cprov)
2076 >>> print_archive_names(ubuntu_copy_archives)2076 >>> print_archive_names(ubuntu_copy_archives)
20772077
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2011-01-20 18:27:00 +0000
+++ lib/lp/soyuz/model/archive.py 2011-02-03 21:26:00 +0000
@@ -80,8 +80,8 @@
80from lp.buildmaster.model.packagebuild import PackageBuild80from lp.buildmaster.model.packagebuild import PackageBuild
81from lp.registry.interfaces.distroseries import IDistroSeriesSet81from lp.registry.interfaces.distroseries import IDistroSeriesSet
82from lp.registry.interfaces.person import (82from lp.registry.interfaces.person import (
83 OPEN_TEAM_POLICY,
83 PersonVisibility,84 PersonVisibility,
84 TeamSubscriptionPolicy,
85 validate_person,85 validate_person,
86 )86 )
87from lp.registry.interfaces.pocket import PackagePublishingPocket87from lp.registry.interfaces.pocket import PackagePublishingPocket
@@ -1704,7 +1704,7 @@
1704 def validatePPA(self, person, proposed_name):1704 def validatePPA(self, person, proposed_name):
1705 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu1705 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1706 if person.isTeam() and (1706 if person.isTeam() and (
1707 person.subscriptionpolicy == TeamSubscriptionPolicy.OPEN):1707 person.subscriptionpolicy in OPEN_TEAM_POLICY):
1708 return "Open teams cannot have PPAs."1708 return "Open teams cannot have PPAs."
1709 if proposed_name is not None and proposed_name == ubuntu.name:1709 if proposed_name is not None and proposed_name == ubuntu.name:
1710 return (1710 return (

Subscribers

People subscribed via source and target branches

to status/vote changes: