← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/no-open-pillar-owners-879103 into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/no-open-pillar-owners-879103 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/no-open-pillar-owners-879103/+merge/81228

Pillars (products, projects, distros) cannot be created with an open team as the maintainer. The maintainer must be a person or restricted/moderated team.

== Implementation ==

1. Add new storm validator: validate_person_or_closed_team

Implementation based on existing person validators.
Change pillar owner and security_contact model properties to use new validator, replacing validate_public_person

2. Add new helper method: is_person_or_closed_team
Implementation based on is_public_person

3. Add new vocab: ValidPillarOwner

Implementation based on ValidPersonOrTeam vocab.
extra_clause property returns: Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY)

4. Change nominated vocab on owner and security property interface fields to use the new vocab , replacing ValidOwner

The changes mean that when existing products and distros are edited (via +review), if they currently have an open team for owner etc, then the form cannot be saved. So this change cannot land until all 233 products and 1 distro with open teams as owner/security contact have been updated.

The next branch will prevent teams which are pillar owners or security contacts from having their subscription policy changed to open.

== Demo and QA ==

Register a new project. The owner picker should say "Search for a restricted team, a moderated team, or a person" and only people and closed teams should be available in the search results.

== Tests ==

Update lib/registry/doc/vocabularies.txt

Add new tests to TestDistribution, TestProduct, TestProjectGroup:
- test_owner_cannot_be_open_team
- test_owner_can_be_closed_team
- test_security_contact_cannot_be_open_team
- test_security_contact_can_be_closed_team

Add new test case for is_person_or_closed_team helper method:
Test_is_person_or_closed_team

Add new test case: TestValidPersonOrClosedTeamVocabulary
- refactor TestValidPersonOrTeamVocabulary, factor out common tests to TestValidPersonOrTeamVocabularyMixin
- add test_team_filter

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/interfaces/securitycontact.py
  lib/lp/registry/errors.py
  lib/lp/registry/vocabularies.py
  lib/lp/registry/vocabularies.zcml
  lib/lp/registry/doc/vocabularies.txt
  lib/lp/registry/interfaces/distribution.py
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/interfaces/product.py
  lib/lp/registry/interfaces/projectgroup.py
  lib/lp/registry/model/distribution.py
  lib/lp/registry/model/product.py
  lib/lp/registry/model/projectgroup.py
  lib/lp/registry/tests/test_distribution.py
  lib/lp/registry/tests/test_person_vocabularies.py
  lib/lp/registry/tests/test_product.py
  lib/lp/registry/tests/test_projectgroup.py
  lib/lp/services/fields/__init__.py
  lib/lp/services/fields/tests/test_fields.py
-- 
https://code.launchpad.net/~wallyworld/launchpad/no-open-pillar-owners-879103/+merge/81228
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/no-open-pillar-owners-879103 into lp:launchpad.
=== modified file 'lib/lp/bugs/interfaces/securitycontact.py'
--- lib/lp/bugs/interfaces/securitycontact.py	2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/interfaces/securitycontact.py	2011-11-07 00:42:24 +0000
@@ -28,4 +28,4 @@
             "security-related bug reports.  The security contact will be "
             "subscribed to all bugs marked as a security vulnerability and "
             "will receive email about all activity on all security bugs."),
-        required=False, vocabulary='ValidPersonOrTeam'))
+        required=False, vocabulary='ValidPillarOwner'))

=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt	2011-10-21 00:06:01 +0000
+++ lib/lp/registry/doc/vocabularies.txt	2011-11-07 00:42:24 +0000
@@ -975,6 +975,28 @@
      u'simple-team', u'testing-spanish-team', u'ubuntu-security',
      u'ubuntu-team', u'warty-gnome']
 
+ValidPillarOwner
+..........
+
+Valid persons and closed teams are valid pillar owners.
+
+    >>> login(ANONYMOUS)
+    >>> vocab = get_naked_vocab(None, "ValidPillarOwner")
+    >>> vocab.step_title
+    'Search for a restricted team, a moderated team, or a person'
+
+    >>> list(vocab.search(None))
+    []
+
+Almost all teams have the word 'team' as part of their names, so a
+search for 'team' should give us some of them. Only restricted or moderated
+teams will be returned in the search results. There's also the ircperson
+created earlier with an icr nickname of 'team':
+
+    >>> sorted(person.name for person in vocab.search('team'))
+    [u'hwdb-team', u'ircperson', u'name18', u'name20', u'name21',
+     u'no-team-memberships', u'otherteam',
+     u'simple-team', u'testing-spanish-team', u'ubuntu-team', u'warty-gnome']
 
 ValidTeam
 .........

=== modified file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py	2011-08-11 20:37:16 +0000
+++ lib/lp/registry/errors.py	2011-11-07 00:42:24 +0000
@@ -16,6 +16,7 @@
     'NameAlreadyTaken',
     'NoSuchDistroSeries',
     'NoSuchSourcePackageName',
+    'OpenTeamLinkageError',
     'PPACreationError',
     'PrivatePersonLinkageError',
     'TeamMembershipTransitionError',
@@ -38,6 +39,11 @@
     """An attempt was made to link a private person/team to something."""
 
 
+@error_status(httplib.FORBIDDEN)
+class OpenTeamLinkageError(ValueError):
+    """An attempt was made to link an open team to something."""
+
+
 @error_status(httplib.CONFLICT)
 class NameAlreadyTaken(Exception):
     """The name given for a person is already in use by other person."""

=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py	2011-08-29 00:13:22 +0000
+++ lib/lp/registry/interfaces/distribution.py	2011-11-07 00:42:24 +0000
@@ -211,7 +211,7 @@
         exported_as='domain_name')
     owner = exported(
         PublicPersonChoice(
-            title=_("Owner"), vocabulary='ValidOwner',
+            title=_("Owner"), vocabulary='ValidPillarOwner',
             description=_("The distro's owner."), required=True))
     registrant = exported(
         PublicPersonChoice(

=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py	2011-10-31 07:59:32 +0000
+++ lib/lp/registry/interfaces/person.py	2011-11-07 00:42:24 +0000
@@ -37,6 +37,7 @@
     'TeamMembershipRenewalPolicy',
     'TeamSubscriptionPolicy',
     'validate_person',
+    'validate_person_or_closed_team',
     'validate_public_person',
     ]
 
@@ -126,6 +127,7 @@
     )
 from lp.code.interfaces.hasrecipes import IHasRecipes
 from lp.registry.errors import (
+    OpenTeamLinkageError,
     PrivatePersonLinkageError,
     TeamSubscriptionPolicyError,
     )
@@ -151,6 +153,7 @@
 from lp.services.fields import (
     BlacklistableContentNameField,
     IconImageUpload,
+    is_public_person_or_closed_team,
     is_public_person,
     LogoImageUpload,
     MugshotImageUpload,
@@ -170,7 +173,8 @@
 
 
 @block_implicit_flushes
-def validate_person_common(obj, attr, value, validate_func):
+def validate_person_common(obj, attr, value, validate_func,
+                           error_class=PrivatePersonLinkageError):
     """Validate the person using the supplied function."""
     if value is None:
         return None
@@ -181,7 +185,7 @@
     from lp.registry.model.person import Person
     person = Person.get(value)
     if not validate_func(person):
-        raise PrivatePersonLinkageError(
+        raise error_class(
             "Cannot link person (name=%s, visibility=%s) to %s (name=%s)"
             % (person.name, person.visibility.name,
                obj, getattr(obj, 'name', None)))
@@ -206,6 +210,15 @@
     return validate_person_common(obj, attr, value, validate)
 
 
+def validate_person_or_closed_team(obj, attr, value):
+
+    def validate(person):
+        return is_public_person_or_closed_team(person)
+
+    return validate_person_common(
+        obj, attr, value, validate, error_class=OpenTeamLinkageError)
+
+
 class PersonalStanding(DBEnumeratedType):
     """A person's standing.
 

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2011-10-29 18:23:06 +0000
+++ lib/lp/registry/interfaces/product.py	2011-11-07 00:42:24 +0000
@@ -440,9 +440,9 @@
         PersonChoice(
             title=_('Maintainer'),
             required=True,
-            vocabulary='ValidOwner',
-            description=_("The person or team who maintains the project "
-                          "information in Launchpad.")))
+            vocabulary='ValidPillarOwner',
+            description=_("The person or closed team who maintains the "
+                          "project information in Launchpad.")))
 
     registrant = exported(
         PublicPersonChoice(

=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
--- lib/lp/registry/interfaces/projectgroup.py	2011-10-07 16:08:17 +0000
+++ lib/lp/registry/interfaces/projectgroup.py	2011-11-07 00:42:24 +0000
@@ -131,9 +131,9 @@
         PublicPersonChoice(
             title=_('Maintainer'),
             required=True,
-            vocabulary='ValidOwner',
+            vocabulary='ValidPillarOwner',
             description=_("Project group owner. Must be either a "
-                          "Launchpad Person or Team.")))
+                          "Launchpad Person or closed Team.")))
 
     registrant = exported(
         PublicPersonChoice(

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2011-10-07 16:20:20 +0000
+++ lib/lp/registry/model/distribution.py	2011-11-07 00:42:24 +0000
@@ -131,6 +131,7 @@
     )
 from lp.registry.interfaces.packaging import PackagingType
 from lp.registry.interfaces.person import (
+    validate_person_or_closed_team,
     validate_person,
     validate_public_person,
     )
@@ -229,7 +230,7 @@
     domainname = StringCol(notNull=True)
     owner = ForeignKey(
         dbName='owner', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
+        storm_validator=validate_person_or_closed_team, notNull=True)
     registrant = ForeignKey(
         dbName='registrant', foreignKey='Person',
         storm_validator=validate_public_person, notNull=True)
@@ -242,7 +243,7 @@
     bug_reported_acknowledgement = StringCol(default=None)
     security_contact = ForeignKey(
         dbName='security_contact', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=False,
+        storm_validator=validate_person_or_closed_team, notNull=False,
         default=None)
     driver = ForeignKey(
         dbName="driver", foreignKey="Person",

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2011-10-29 18:23:06 +0000
+++ lib/lp/registry/model/product.py	2011-11-07 00:42:24 +0000
@@ -141,6 +141,7 @@
 from lp.registry.interfaces.person import (
     IPersonSet,
     validate_person,
+    validate_person_or_closed_team,
     validate_public_person,
     )
 from lp.registry.interfaces.pillar import IPillarNameSet
@@ -318,7 +319,7 @@
         default=None)
     _owner = ForeignKey(
         dbName="owner", foreignKey="Person",
-        storm_validator=validate_person,
+        storm_validator=validate_person_or_closed_team,
         notNull=True)
     registrant = ForeignKey(
         dbName="registrant", foreignKey="Person",
@@ -331,7 +332,7 @@
         default=None)
     security_contact = ForeignKey(
         dbName='security_contact', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=False,
+        storm_validator=validate_person_or_closed_team, notNull=False,
         default=None)
     driver = ForeignKey(
         dbName="driver", foreignKey="Person",

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2011-10-07 16:20:20 +0000
+++ lib/lp/registry/model/projectgroup.py	2011-11-07 00:42:24 +0000
@@ -84,7 +84,10 @@
     HasBranchesMixin,
     HasMergeProposalsMixin,
     )
-from lp.registry.interfaces.person import validate_public_person
+from lp.registry.interfaces.person import (
+    validate_person_or_closed_team,
+    validate_public_person,
+    )
 from lp.registry.interfaces.pillar import IPillarNameSet
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.projectgroup import (
@@ -127,7 +130,7 @@
     # db field names
     owner = ForeignKey(
         dbName='owner', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
+        storm_validator=validate_person_or_closed_team, notNull=True)
     registrant = ForeignKey(
         dbName='registrant', foreignKey='Person',
         storm_validator=validate_public_person, notNull=True)

=== modified file 'lib/lp/registry/tests/test_distribution.py'
--- lib/lp/registry/tests/test_distribution.py	2011-10-12 11:10:39 +0000
+++ lib/lp/registry/tests/test_distribution.py	2011-11-07 00:42:24 +0000
@@ -27,7 +27,14 @@
 from lp.app.enums import ServiceUsage
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-from lp.registry.errors import NoSuchDistroSeries
+from lp.registry.errors import (
+    NoSuchDistroSeries,
+    OpenTeamLinkageError,
+    )
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    OPEN_TEAM_POLICY,
+    )
 from lp.registry.interfaces.distribution import (
     IDistribution,
     IDistributionSet,
@@ -61,6 +68,34 @@
         distro = self.factory.makeDistribution()
         self.assertEqual("Distribution", distro.pillar_category)
 
+    def test_owner_cannot_be_open_team(self):
+        """Distro owners cannot be open teams."""
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertRaises(
+                OpenTeamLinkageError, self.factory.makeDistribution,
+                owner=open_team)
+
+    def test_owner_can_be_closed_team(self):
+        """Distro owners can be closed teams."""
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.factory.makeDistribution(owner=closed_team)
+
+    def test_security_contact_cannot_be_open_team(self):
+        """Distro security contacts cannot be open teams."""
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertRaises(
+                OpenTeamLinkageError, self.factory.makeDistribution,
+                security_contact=open_team)
+
+    def test_security_contact_can_be_closed_team(self):
+        """Distro security contacts can be closed teams."""
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.factory.makeDistribution(security_contact=closed_team)
+
     def test_distribution_repr_ansii(self):
         # Verify that ANSI displayname is ascii safe.
         distro = self.factory.makeDistribution(

=== modified file 'lib/lp/registry/tests/test_person_vocabularies.py'
--- lib/lp/registry/tests/test_person_vocabularies.py	2011-10-20 00:53:01 +0000
+++ lib/lp/registry/tests/test_person_vocabularies.py	2011-11-07 00:42:24 +0000
@@ -21,6 +21,8 @@
 from lp.registry.interfaces.person import (
     PersonVisibility,
     TeamSubscriptionPolicy,
+    CLOSED_TEAM_POLICY,
+    OPEN_TEAM_POLICY,
     )
 from lp.registry.interfaces.karma import IKarmaCacheManager
 from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
@@ -51,15 +53,8 @@
         return removeSecurityProxy(vocabulary).search(text, vocab_filter)
 
 
-class TestValidPersonOrTeamVocabulary(VocabularyTestBase,
-                                      TestCaseWithFactory):
-    """Test that the ValidPersonOrTeamVocabulary behaves as expected.
-
-    Most tests are in lib/lp/registry/doc/vocabularies.txt.
-    """
-
-    layer = LaunchpadZopelessLayer
-    vocabulary_name = 'ValidPersonOrTeam'
+class ValidPersonOrTeamVocabularyMixin(VocabularyTestBase):
+    """Common tests for the ValidPersonOrTeam vocabulary derivatives."""
 
     def test_supported_filters(self):
         # The vocab supports the correct filters.
@@ -156,12 +151,23 @@
             name="fredteam", email="fredteam@xxxxxxx")
         self._person_filter_tests(person)
 
-    def _team_filter_tests(self, team):
+    def _team_filter_tests(self, teams):
         results = self.searchVocabulary(None, '', 'TEAM')
         for personorteam in results:
             self.assertTrue(personorteam.is_team)
         results = self.searchVocabulary(None, u'fred', 'TEAM')
-        self.assertEqual([team], list(results))
+        self.assertContentEqual(teams, list(results))
+
+
+class TestValidPersonOrTeamVocabulary(ValidPersonOrTeamVocabularyMixin,
+                                      TestCaseWithFactory):
+    """Test that the ValidPersonOrTeamVocabulary behaves as expected.
+
+    Most tests are in lib/lp/registry/doc/vocabularies.txt.
+    """
+
+    layer = LaunchpadZopelessLayer
+    vocabulary_name = 'ValidPersonOrTeam'
 
     def test_team_filter(self):
         # Test that the team filter only returns teams.
@@ -169,8 +175,7 @@
             name="fredperson", email="fredperson@xxxxxxx")
         team = self.factory.makeTeam(
             name="fredteam", email="fredteam@xxxxxxx")
-        self._team_filter_tests(team)
-        self._team_filter_tests(team)
+        self._team_filter_tests([team])
 
 
 class TestValidPersonOrTeamPreloading(VocabularyTestBase,
@@ -209,6 +214,31 @@
         self.assertThat(recorder, HasQueryCount(Equals(0)))
 
 
+class TestValidPersonOrClosedTeamVocabulary(ValidPersonOrTeamVocabularyMixin,
+                                            TestCaseWithFactory):
+    """Test that the ValidPersonOrClosedTeamVocabulary behaves as expected."""
+
+    layer = LaunchpadZopelessLayer
+    vocabulary_name = 'ValidPillarOwner'
+
+    def test_team_filter(self):
+        # Test that the team filter only returns closed teams.
+        self.factory.makePerson(
+            name="fredperson", email="fredperson@xxxxxxx")
+        for policy in OPEN_TEAM_POLICY:
+            self.factory.makeTeam(
+                name="fred%s" % policy.name.lower(),
+                email="team_%s@xxxxxxx" % policy.name,
+                subscription_policy=policy)
+        closed_teams = []
+        for policy in CLOSED_TEAM_POLICY:
+            closed_teams.append(self.factory.makeTeam(
+                name="fred%s" % policy.name.lower(),
+                email="team_%s@xxxxxxx" % policy.name,
+                subscription_policy=policy))
+        self._team_filter_tests(closed_teams)
+
+
 class TeamMemberVocabularyTestBase(VocabularyTestBase):
 
     def test_open_team_cannot_be_a_member_of_a_closed_team(self):

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2011-10-18 12:57:33 +0000
+++ lib/lp/registry/tests/test_product.py	2011-11-07 00:42:24 +0000
@@ -34,10 +34,15 @@
 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
 from lp.bugs.interfaces.bugtarget import IHasBugHeat
+from lp.registry.errors import OpenTeamLinkageError
 from lp.registry.interfaces.product import (
     IProduct,
     License,
     )
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    OPEN_TEAM_POLICY,
+    )
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.commercialsubscription import CommercialSubscription
 from lp.registry.model.product import (
@@ -222,6 +227,34 @@
             [u'trunk', u'active-series'],
             [series.name for series in active_series])
 
+    def test_owner_cannot_be_open_team(self):
+        """Product owners cannot be open teams."""
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertRaises(
+                OpenTeamLinkageError, self.factory.makeProduct,
+                owner=open_team)
+
+    def test_owner_can_be_closed_team(self):
+        """Product owners can be closed teams."""
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.factory.makeProduct(owner=closed_team)
+
+    def test_security_contact_cannot_be_open_team(self):
+        """Product security contacts cannot be open teams."""
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertRaises(
+                OpenTeamLinkageError, self.factory.makeProduct,
+                security_contact=open_team)
+
+    def test_security_contact_can_be_closed_team(self):
+        """Product security contacts can be closed teams."""
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.factory.makeProduct(security_contact=closed_team)
+
 
 class TestProductFiles(TestCase):
     """Tests for downloadable product files."""

=== modified file 'lib/lp/registry/tests/test_projectgroup.py'
--- lib/lp/registry/tests/test_projectgroup.py	2011-10-25 04:43:25 +0000
+++ lib/lp/registry/tests/test_projectgroup.py	2011-11-07 00:42:24 +0000
@@ -11,6 +11,11 @@
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     )
+from lp.registry.errors import OpenTeamLinkageError
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    OPEN_TEAM_POLICY,
+    )
 from lp.registry.interfaces.projectgroup import IProjectGroupSet
 from lp.testing import (
     launchpadlib_for,
@@ -30,6 +35,20 @@
         pg = self.factory.makeProject()
         self.assertEqual("Project Group", pg.pillar_category)
 
+    def test_owner_cannot_be_open_team(self):
+        """Project group owners cannot be open teams."""
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertRaises(
+                OpenTeamLinkageError, self.factory.makeProject,
+                owner=open_team)
+
+    def test_owner_can_be_closed_team(self):
+        """Project group owners can be closed teams."""
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.factory.makeProject(owner=closed_team)
+
 
 class ProjectGroupSearchTestCase(TestCaseWithFactory):
 

=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py	2011-10-20 00:53:01 +0000
+++ lib/lp/registry/vocabularies.py	2011-11-07 00:42:24 +0000
@@ -886,6 +886,23 @@
             return 'Search'
 
 
+class ValidPersonOrClosedTeamVocabulary(TeamVocabularyMixin,
+                                ValidPersonOrTeamVocabulary):
+    """The set of people and closed teams in Launchpad.
+
+    A closed team is one for which the subscription policy is either
+    RESTRICTED or MODERATED.
+    """
+
+    @property
+    def is_closed_team(self):
+        return True
+
+    @property
+    def extra_clause(self):
+        return Person.subscriptionpolicy.is_in(CLOSED_TEAM_POLICY)
+
+
 class ValidTeamMemberVocabulary(TeamVocabularyMixin,
                                 ValidPersonOrTeamVocabulary):
     """The set of valid members of a given team.

=== modified file 'lib/lp/registry/vocabularies.zcml'
--- lib/lp/registry/vocabularies.zcml	2011-08-22 13:01:14 +0000
+++ lib/lp/registry/vocabularies.zcml	2011-11-07 00:42:24 +0000
@@ -355,6 +355,17 @@
     <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
   </securedutility>
 
+  <securedutility
+    name="ValidPillarOwner"
+    component="lp.registry.vocabularies.ValidPersonOrClosedTeamVocabulary"
+    provides="zope.schema.interfaces.IVocabularyFactory"
+    >
+    <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
+  </securedutility>
+
+  <class class="lp.registry.vocabularies.ValidPersonOrClosedTeamVocabulary">
+    <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/>
+  </class>
 
   <securedutility
     name="ValidTeam"

=== modified file 'lib/lp/services/fields/__init__.py'
--- lib/lp/services/fields/__init__.py	2011-02-24 15:30:54 +0000
+++ lib/lp/services/fields/__init__.py	2011-11-07 00:42:24 +0000
@@ -55,6 +55,7 @@
     'URIField',
     'UniqueField',
     'Whiteboard',
+    'is_public_person_or_closed_team',
     'is_public_person',
     ]
 
@@ -243,6 +244,7 @@
 class NoneableTextLine(StrippedTextLine):
     implements(INoneableTextLine)
 
+
 # Title
 # A field to capture a launchpad object title
 
@@ -278,7 +280,6 @@
         return super(StrippableText, self).validate(value)
 
 
-
 # Summary
 # A field capture a Launchpad object summary
 
@@ -770,19 +771,19 @@
 class IconImageUpload(BaseImageUpload):
 
     dimensions = (14, 14)
-    max_size = 5*1024
+    max_size = 5 * 1024
 
 
 class LogoImageUpload(BaseImageUpload):
 
     dimensions = (64, 64)
-    max_size = 50*1024
+    max_size = 50 * 1024
 
 
 class MugshotImageUpload(BaseImageUpload):
 
     dimensions = (192, 192)
-    max_size = 100*1024
+    max_size = 100 * 1024
 
 
 class LocationField(Field):
@@ -830,6 +831,20 @@
     return person.visibility == PersonVisibility.PUBLIC
 
 
+def is_public_person_or_closed_team(person):
+    """Return True if person is a Person or not an open or delegated team."""
+    from lp.registry.interfaces.person import (
+        IPerson,
+        PersonVisibility,
+        CLOSED_TEAM_POLICY,
+    )
+    if not IPerson.providedBy(person):
+        return False
+    if not person.is_team:
+        return person.visibility == PersonVisibility.PUBLIC
+    return person.subscriptionpolicy in CLOSED_TEAM_POLICY
+
+
 class PrivateTeamNotAllowed(ConstraintNotSatisfied):
     __doc__ = _("A private team is not allowed.")
 

=== modified file 'lib/lp/services/fields/tests/test_fields.py'
--- lib/lp/services/fields/tests/test_fields.py	2011-02-24 15:30:54 +0000
+++ lib/lp/services/fields/tests/test_fields.py	2011-11-07 00:42:24 +0000
@@ -17,12 +17,16 @@
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.app.validators import LaunchpadValidationError
 from lp.registry.interfaces.nameblacklist import INameBlacklistSet
+from lp.registry.interfaces.person import (
+    CLOSED_TEAM_POLICY,
+    OPEN_TEAM_POLICY,
+    )
 from lp.services.fields import (
     BaseImageUpload,
     BlacklistableContentNameField,
     FormattableDate,
     StrippableText,
-    )
+    is_public_person_or_closed_team)
 from lp.testing import (
     login_person,
     TestCase,
@@ -159,7 +163,7 @@
 
     class ExampleImageUpload(BaseImageUpload):
         dimensions = (192, 192)
-        max_size = 100*1024
+        max_size = 100 * 1024
 
     def test_validation_corrupt_image(self):
         # ValueErrors raised by PIL become LaunchpadValidationErrors.
@@ -182,3 +186,30 @@
         image.filename = 'foo.jpg'
         self.assertRaises(
             LaunchpadValidationError, field.validate, image)
+
+
+class Test_is_person_or_closed_team(TestCaseWithFactory):
+    """ Tests for is_person_or_closed_team()."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_non_person(self):
+        self.assertFalse(is_public_person_or_closed_team(0))
+
+    def test_person(self):
+        person = self.factory.makePerson()
+        self.assertTrue(is_public_person_or_closed_team(person))
+
+    def test_open_team(self):
+        for policy in OPEN_TEAM_POLICY:
+            open_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertFalse(
+                is_public_person_or_closed_team(open_team),
+                "%s is not open" % policy)
+
+    def test_closed_team(self):
+        for policy in CLOSED_TEAM_POLICY:
+            closed_team = self.factory.makeTeam(subscription_policy=policy)
+            self.assertTrue(
+                is_public_person_or_closed_team(closed_team),
+                "%s is not closed" % policy)