← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/bug-588773-take2 into lp:launchpad/devel

 

Brad Crittenden has proposed merging lp:~bac/launchpad/bug-588773-take2 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #588773 Allow Registry Admins to change pillar and person names
  https://bugs.launchpad.net/bugs/588773


= Summary =

Registry experts need the ability to change pillar names but do not need
full admin privileges.

== Proposed fix ==

Each pillar has a +edit page which requires owner or admin privileges.
They also have another page, sometimes called +admin and sometimes
+review, that allows more limited changes.  Giving registry experts
access to those pages solves the problem without going overboard.

Sorry for exceeding the size limit.

== Pre-implementation notes ==

Chats with Curtis.

== Implementation details ==

As above.

== Tests ==

bin/test -vv -t xx-product-edit.txt -t xx-project-edit.txt \
  -t xx-admin-person-review.txt -t distroseries-admin.txt



= Launchpad lint =

This are either bogus our outside the scope of my already large branch.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/templates/product-index.pt
  lib/lp/registry/browser/tests/peoplemerge-views.txt
  lib/canonical/launchpad/security.py
  lib/canonical/launchpad/permissions.zcml
  lib/lp/registry/vocabularies.py
  lib/lp/registry/doc/commercialsubscription.txt
  lib/lp/registry/templates/project-listing-detailed.pt
  lib/lp/testing/sampledata.py
  lib/lp/registry/browser/product.py
  lib/lp/answers/browser/configure.zcml
  lib/lp/registry/browser/tests/product-views.txt
  lib/lp/registry/configure.zcml
  lib/lp/registry/interfaces/product.py
  lib/lp/registry/doc/vocabularies.txt
  lib/lp/registry/stories/project/xx-project-edit.txt
  lib/lp/registry/stories/product/xx-product-edit.txt
  lib/lp/registry/stories/person/xx-admin-person-review.txt
  lib/lp/testing/factory.py
  lib/canonical/launchpad/webapp/tests/test_login.py
  lib/lp/registry/browser/tests/projectgroupset-views.txt
  lib/lp/registry/stories/distroseries/distroseries-admin.txt
  lib/lp/registry/browser/person.py
  lib/lp/registry/browser/distribution.py
  lib/lp/registry/browser/distroseries.py
  lib/lp/registry/browser/project.py

./lib/canonical/launchpad/security.py
     649: E302 expected 2 blank lines, found 1
    1268: E302 expected 2 blank lines, found 1
    1492: E302 expected 2 blank lines, found 1
./lib/lp/registry/vocabularies.py
     180: E302 expected 2 blank lines, found 1
     223: E202 whitespace before ')'
     231: E302 expected 2 blank lines, found 1
     271: E202 whitespace before ')'
     548: E202 whitespace before ')'
     630: E302 expected 2 blank lines, found 1
     983: E202 whitespace before ')'
    1271: E202 whitespace before ']'
    1410: E202 whitespace before ')'
    1428: E202 whitespace before ')'
    1507: E301 expected 1 blank line, found 0
./lib/lp/registry/interfaces/product.py
     901: E301 expected 1 blank line, found 2
./lib/lp/registry/doc/vocabularies.txt
       1: narrative uses a moin header.
      24: narrative uses a moin header.
     182: narrative uses a moin header.
     226: narrative uses a moin header.
     312: narrative uses a moin header.
     337: narrative uses a moin header.
     495: narrative uses a moin header.
     509: narrative uses a moin header.
     543: narrative uses a moin header.
     584: narrative uses a moin header.
     602: narrative uses a moin header.
     693: narrative uses a moin header.
     712: narrative uses a moin header.
     744: narrative uses a moin header.
     950: narrative uses a moin header.
    1051: narrative uses a moin header.
    1102: narrative uses a moin header.
    1144: narrative uses a moin header.
    1188: narrative uses a moin header.
    1249: narrative uses a moin header.
    1312: narrative uses a moin header.
    1347: narrative uses a moin header.
./lib/lp/testing/factory.py
     367: E301 expected 1 blank line, found 2
./lib/canonical/launchpad/webapp/tests/test_login.py
      90: E301 expected 1 blank line, found 0
./lib/lp/registry/browser/distribution.py
     295: E231 missing whitespace after ','
     569: E302 expected 2 blank lines, found 1
./lib/lp/registry/browser/project.py
     332: E301 expected 1 blank line, found 2
-- 
https://code.launchpad.net/~bac/launchpad/bug-588773-take2/+merge/32805
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-588773-take2 into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/permissions.zcml'
--- lib/canonical/launchpad/permissions.zcml	2010-07-14 12:31:25 +0000
+++ lib/canonical/launchpad/permissions.zcml	2010-08-16 19:36:15 +0000
@@ -76,15 +76,6 @@
     title="The role of someone who can manage and configure a mailing list."
     access_level="write" />
 
-  <!--
-    XXX: BradCrittenden 2009-05-17 bug=400762: ProjectReview should properly
-    just be Moderate except there is a clash between IProduct and IFAQTarget.
-  -->
-  <permission
-    id="launchpad.ProjectReview"
-    title="Review Launchpad projects"
-    access_level="write" />
-
   <!-- This permission only exists to please apidoc.launchpad.dev and
        shouldn't be used anywhere else. -->
   <permission

=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2010-08-13 01:43:26 +0000
+++ lib/canonical/launchpad/security.py	2010-08-16 19:36:15 +0000
@@ -224,31 +224,35 @@
         return user.in_admin or user.in_registry_experts
 
 
-class ReviewByRegistryExpertsOrAdmins(AuthorizationBase):
-    permission = 'launchpad.ProjectReview'
+class ModerateByRegistryExpertsOrAdmins(AuthorizationBase):
+    permission = 'launchpad.Moderate'
     usedfor = None
 
     def checkAuthenticated(self, user):
         return user.in_admin or user.in_registry_experts
 
 
-class ReviewProduct(ReviewByRegistryExpertsOrAdmins):
+class ModerateDistroSeries(ModerateByRegistryExpertsOrAdmins):
+    usedfor = IDistroSeries
+
+
+class ModerateProduct(ModerateByRegistryExpertsOrAdmins):
     usedfor = IProduct
 
 
-class ReviewProductSet(ReviewByRegistryExpertsOrAdmins):
+class ModerateProductSet(ModerateByRegistryExpertsOrAdmins):
     usedfor = IProductSet
 
 
-class ReviewProject(ReviewByRegistryExpertsOrAdmins):
+class ModerateProject(ModerateByRegistryExpertsOrAdmins):
     usedfor = IProjectGroup
 
 
-class ReviewProjectGroupSet(ReviewByRegistryExpertsOrAdmins):
+class ModerateProjectGroupSet(ModerateByRegistryExpertsOrAdmins):
     usedfor = IProjectGroupSet
 
 
-class ModeratePerson(ReviewByRegistryExpertsOrAdmins):
+class ModeratePerson(ModerateByRegistryExpertsOrAdmins):
     permission = 'launchpad.Moderate'
     usedfor = IPerson
 
@@ -588,7 +592,7 @@
         return user.in_admin
 
 
-class ModeratePersonSetByExpertsOrAdmins(ReviewByRegistryExpertsOrAdmins):
+class ModeratePersonSetByExpertsOrAdmins(ModerateByRegistryExpertsOrAdmins):
     permission = 'launchpad.Moderate'
     usedfor = IPersonSet
 
@@ -615,7 +619,7 @@
         return can_edit_team(self.obj, user)
 
 
-class ModerateTeam(ReviewByRegistryExpertsOrAdmins):
+class ModerateTeam(ModerateByRegistryExpertsOrAdmins):
     permission = 'launchpad.Moderate'
     usedfor = ITeam
 
@@ -1668,8 +1672,8 @@
         return user.inTeam(self.obj.owner)
 
 
-class ModerateFAQTarget(EditByOwnersOrAdmins):
-    permission = 'launchpad.Moderate'
+class AppendFAQTarget(EditByOwnersOrAdmins):
+    permission = 'launchpad.Append'
     usedfor = IFAQTarget
 
     def checkAuthenticated(self, user):
@@ -1688,9 +1692,9 @@
     usedfor = IFAQ
 
     def checkAuthenticated(self, user):
-        """Everybody who has launchpad.Moderate on the FAQ target is allowed.
+        """Everybody who has launchpad.Append on the FAQ target is allowed.
         """
-        return ModerateFAQTarget(self.obj.target).checkAuthenticated(user)
+        return AppendFAQTarget(self.obj.target).checkAuthenticated(user)
 
 
 def can_edit_team(team, user):

=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
--- lib/canonical/launchpad/webapp/tests/test_login.py	2010-08-12 21:26:53 +0000
+++ lib/canonical/launchpad/webapp/tests/test_login.py	2010-08-16 19:36:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  All rights reserved.
+# Copyright 2009-2010 Canonical Ltd.  All rights reserved.
 from __future__ import with_statement
 
 # pylint: disable-msg=W0105
@@ -70,7 +70,7 @@
 
 
 class FakeConsumer:
-    """An OpenID consumer that stashes away arguments for test instection."""
+    """An OpenID consumer that stashes away arguments for test inspection."""
 
     def complete(self, params, requested_url):
         self.params = params
@@ -268,7 +268,7 @@
                 'foo': 'bar',
             })
         self.assertEquals(
-            view.fake_consumer.requested_url,'http://example.com?foo=bar')
+            view.fake_consumer.requested_url, 'http://example.com?foo=bar')
 
     def test_personless_account(self):
         # When there is no Person record associated with the account, we

=== modified file 'lib/lp/answers/browser/configure.zcml'
--- lib/lp/answers/browser/configure.zcml	2010-07-27 17:18:24 +0000
+++ lib/lp/answers/browser/configure.zcml	2010-08-16 19:36:15 +0000
@@ -308,7 +308,7 @@
     name="+createfaq"
     for="lp.answers.interfaces.faqtarget.IFAQTarget"
     class=".faqtarget.FAQCreateView"
-    permission="launchpad.Moderate"
+    permission="launchpad.Append"
     template="../../app/templates/generic-edit.pt"
     />
 

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-07-16 16:58:55 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-08-16 19:36:15 +0000
@@ -126,7 +126,7 @@
         template="../../app/templates/generic-edit.pt"/>
     <browser:page
         for="lp.registry.interfaces.distroseries.IDistroSeries"
-        permission="launchpad.Admin"
+        permission="launchpad.Moderate"
         name="+admin"
         class="lp.registry.browser.distroseries.DistroSeriesAdminView"
         facet="overview"
@@ -362,7 +362,7 @@
         for="lp.registry.interfaces.projectgroup.IProjectGroup"
         class="lp.registry.browser.project.ProjectReviewView"
         facet="overview"
-        permission="launchpad.Admin"
+        permission="launchpad.Moderate"
         template="../templates/project-edit.pt"/>
     <browser:page
         name="+newproduct"
@@ -399,7 +399,7 @@
         for="lp.registry.interfaces.projectgroup.IProjectGroupSet"
         class="lp.registry.browser.project.ProjectAddView"
         facet="overview"
-        permission="launchpad.ProjectReview"
+        permission="launchpad.Moderate"
         template="../../app/templates/generic-edit.pt"/>
     <browser:url
         for="lp.registry.interfaces.projectgroup.IProjectGroupSeries"
@@ -775,7 +775,7 @@
             name="+review"
             for="lp.registry.interfaces.person.IPerson"
             class="lp.registry.browser.person.PersonAdministerView"
-            permission="launchpad.Admin"
+            permission="launchpad.Moderate"
             template="../templates/person-review.pt"/>
         <browser:page
             name="+reviewaccount"
@@ -1502,14 +1502,14 @@
         for="lp.registry.interfaces.product.IProduct"
         facet="overview"
         class="lp.registry.browser.product.ProductAdminView"
-        permission="launchpad.Admin"
+        permission="launchpad.Moderate"
         template="../../app/templates/generic-edit.pt"/>
     <browser:page
         name="+review-license"
         for="lp.registry.interfaces.product.IProduct"
         facet="overview"
         class="lp.registry.browser.product.ProductReviewLicenseView"
-        permission="launchpad.ProjectReview"
+        permission="launchpad.Moderate"
         template="../templates/product-review-license.pt"/>
     <browser:page
         name="+addseries"
@@ -1546,7 +1546,7 @@
         for="lp.registry.interfaces.product.IProductSet"
         class="lp.registry.browser.product.ProductSetReviewLicensesView"
         facet="overview"
-        permission="launchpad.ProjectReview">
+        permission="launchpad.Moderate">
         <browser:page
             name="+review-licenses"
             template="../templates/products-review-licenses.pt"/>

=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2010-08-04 23:33:30 +0000
+++ lib/lp/registry/browser/distribution.py	2010-08-16 19:36:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for distributions."""
@@ -17,7 +17,6 @@
     'DistributionDisabledMirrorsView',
     'DistributionEditView',
     'DistributionFacets',
-    'DistributionLanguagePackAdminView',
     'DistributionNavigation',
     'DistributionPPASearchView',
     'DistributionPackageSearchView',

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2010-08-10 02:56:06 +0000
+++ lib/lp/registry/browser/distroseries.py	2010-08-16 19:36:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """View classes related to `IDistroSeries`."""
@@ -190,7 +190,7 @@
         text = 'Add architecture'
         return Link('+addport', text, icon='add')
 
-    @enabled_with_permission('launchpad.Admin')
+    @enabled_with_permission('launchpad.Moderate')
     def admin(self):
         text = 'Administer'
         return Link('+admin', text, icon='edit')

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2010-08-13 07:48:54 +0000
+++ lib/lp/registry/browser/person.py	2010-08-16 19:36:15 +0000
@@ -903,7 +903,7 @@
         text = 'Change details'
         return Link(target, text, icon='edit')
 
-    @enabled_with_permission('launchpad.Admin')
+    @enabled_with_permission('launchpad.Moderate')
     def administer(self):
         target = '+review'
         text = 'Administer'

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2010-08-10 20:02:08 +0000
+++ lib/lp/registry/browser/product.py	2010-08-16 19:36:15 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for products."""
@@ -495,12 +495,12 @@
         text = 'Change people'
         return Link('+edit-people', text, icon='edit')
 
-    @enabled_with_permission('launchpad.ProjectReview')
+    @enabled_with_permission('launchpad.Moderate')
     def review_license(self):
         text = 'Review project'
         return Link('+review-license', text, icon='edit')
 
-    @enabled_with_permission('launchpad.Admin')
+    @enabled_with_permission('launchpad.Moderate')
     def administer(self):
         text = 'Administer'
         return Link('+admin', text, icon='edit')
@@ -1683,7 +1683,7 @@
         'view_all_projects',
         ]
 
-    @enabled_with_permission('launchpad.ProjectReview')
+    @enabled_with_permission('launchpad.Moderate')
     def review_licenses(self):
         return Link('+review-licenses', 'Review projects', icon='edit')
 

=== modified file 'lib/lp/registry/browser/project.py'
--- lib/lp/registry/browser/project.py	2010-08-02 02:13:52 +0000
+++ lib/lp/registry/browser/project.py	2010-08-16 19:36:15 +0000
@@ -115,7 +115,7 @@
     usedfor = IProjectGroupSet
     links = ['register', 'listall']
 
-    @enabled_with_permission('launchpad.ProjectReview')
+    @enabled_with_permission('launchpad.Moderate')
     def register(self):
         text = 'Register a project group'
         return Link('+new', text, icon='add')
@@ -160,7 +160,7 @@
 
 class ProjectAdminMenuMixin:
 
-    @enabled_with_permission('launchpad.Admin')
+    @enabled_with_permission('launchpad.Moderate')
     def administer(self):
         text = 'Administer'
         return Link('+review', text, icon='edit')
@@ -457,7 +457,7 @@
         'view_all_project_groups',
         ]
 
-    @enabled_with_permission('launchpad.ProjectReview')
+    @enabled_with_permission('launchpad.Moderate')
     def register_project_group(self):
         text = 'Register a project group'
         return Link('+new', text, icon='add')

=== modified file 'lib/lp/registry/browser/tests/peoplemerge-views.txt'
--- lib/lp/registry/browser/tests/peoplemerge-views.txt	2010-08-10 04:40:03 +0000
+++ lib/lp/registry/browser/tests/peoplemerge-views.txt	2010-08-16 19:36:15 +0000
@@ -13,10 +13,7 @@
     >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
     >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
     >>> person_set = getUtility(IPersonSet)
-    >>> registry_expert = factory.makePerson()
-    >>> login_person(registry_experts.teamowner)
-    >>> ignored = registry_experts.addMember(
-    ...     registry_expert, registry_experts.teamowner)
+    >>> registry_expert= factory.makeRegistryExpert()
     >>> login_person(registry_expert)
 
 A team (name21) can be merged into another (ubuntu-team).

=== modified file 'lib/lp/registry/browser/tests/product-views.txt'
--- lib/lp/registry/browser/tests/product-views.txt	2010-04-27 19:18:54 +0000
+++ lib/lp/registry/browser/tests/product-views.txt	2010-08-16 19:36:15 +0000
@@ -128,7 +128,7 @@
     >>> view = create_initialized_view(firefox, name='+review-license')
     Traceback (most recent call last):
     ...
-    Unauthorized: (<Product..., 'license_reviewed', 'launchpad.ProjectReview')
+    Unauthorized: (<Product..., 'license_reviewed', 'launchpad.Moderate')
 
 Mark is in the registry admins team and is allowed to access the page.
 
@@ -137,7 +137,8 @@
     >>> print view.label
     Review project
 
-Adding the Commercial Admin to the registry experts team will allow him access.
+Adding the Commercial Admin to the registry experts team will give
+him access.
 
     >>> commercial_member = getUtility(IPersonSet).getByEmail(
     ...     'commercial-member@xxxxxxxxxxxxx')

=== modified file 'lib/lp/registry/browser/tests/projectgroupset-views.txt'
--- lib/lp/registry/browser/tests/projectgroupset-views.txt	2010-02-17 11:19:42 +0000
+++ lib/lp/registry/browser/tests/projectgroupset-views.txt	2010-08-16 19:36:15 +0000
@@ -20,7 +20,7 @@
     >>> view = create_view(project_set, name='+index')
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
 
 The view provides a page_title.
@@ -37,7 +37,7 @@
     >>> user.inTeam(registry)
     False
 
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     False
 
 A member of the registry team has permission.
@@ -49,7 +49,7 @@
     True
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
 
 
@@ -60,7 +60,7 @@
 
     >>> view = create_view(project_set, name='+all')
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
 
 The view provides a page_title.
@@ -71,13 +71,13 @@
 A regular user cannot access the view.
 
     >>> login_person(user)
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     False
 
 A member of the registry team has permission.
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
 
 
@@ -88,7 +88,7 @@
 
     >>> view = create_view(project_set, name='+new')
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
 
     >>> print view.page_title
@@ -97,12 +97,11 @@
 A regular user cannot access the view.
 
     >>> login_person(user)
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     False
 
 A member of the registry team has permission.
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', view)
+    >>> check_permission('launchpad.Moderate', view)
     True
-

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2010-08-06 14:49:35 +0000
+++ lib/lp/registry/configure.zcml	2010-08-16 19:36:15 +0000
@@ -137,7 +137,7 @@
                                  shedloads of data. -->
 
         <require
-            permission="launchpad.Admin"
+            permission="launchpad.Moderate"
             set_attributes="version name status owner nominatedarchindep                                                                                                 changeslist datereleased"/>
 
         <!-- IStructuralSubscriptionTarget -->
@@ -1127,13 +1127,13 @@
             attributes="
                 qualifies_for_free_hosting"/>
         <require
-            permission="launchpad.ProjectReview"
+            permission="launchpad.Moderate"
             attributes="
                 reviewer_whiteboard
                 is_permitted
                 license_reviewed
                 license_approved"
-            set_schema="lp.registry.interfaces.product.IProductProjectReviewRestricted"
+            set_schema="lp.registry.interfaces.product.IProductModerateRestricted"
             set_attributes="active private_bugs "/>
 
         <!-- Changes to official_rosetta must be available to Launchpad
@@ -1275,7 +1275,7 @@
         <allow
             interface="lp.registry.interfaces.product.IProductSet"/>
         <require
-            permission="launchpad.ProjectReview"
+            permission="launchpad.Moderate"
             attributes="
                 forReview"/>
     </securedutility>

=== modified file 'lib/lp/registry/doc/commercialsubscription.txt'
--- lib/lp/registry/doc/commercialsubscription.txt	2010-07-28 22:10:42 +0000
+++ lib/lp/registry/doc/commercialsubscription.txt	2010-08-16 19:36:15 +0000
@@ -566,27 +566,28 @@
     ...     print product.name
     abc.com
 
-The use of 'forReview' is limited to users with launchpad.Commercial permission.
+The use of 'forReview' is limited to users with launchpad.Commercial
+permission.
 
 Anonymous users cannot access 'forReview'.
 
     >>> login(ANONYMOUS)
-    >>> check_permission('launchpad.ProjectReview', product_set)
+    >>> check_permission('launchpad.Moderate', product_set)
     False
     >>> gnome = product_set.forReview(search_text='gnome')
     Traceback (most recent call last):
     ...
-    Unauthorized:... 'forReview', 'launchpad.ProjectReview'...
+    Unauthorized:... 'forReview', 'launchpad.Moderate'...
 
 No Privileges Person cannot access 'forReview'.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> check_permission('launchpad.ProjectReview', product_set)
+    >>> check_permission('launchpad.Moderate', product_set)
     False
     >>> gnome =  product_set.forReview(search_text='gnome')
     Traceback (most recent call last):
     ...
-    Unauthorized:... 'forReview', 'launchpad.ProjectReview'...
+    Unauthorized:... 'forReview', 'launchpad.Moderate'...
 
 But if we add No Privileges Person to the Registry Experts team he can
 access it.
@@ -598,7 +599,7 @@
     >>> logout()
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', product_set)
+    >>> check_permission('launchpad.Moderate', product_set)
     True
     >>> gnome =  product_set.forReview(search_text='gnome')
     >>> for product in gnome:
@@ -612,7 +613,7 @@
 An admin can access 'forReview'.
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> check_permission('launchpad.ProjectReview', product_set)
+    >>> check_permission('launchpad.Moderate', product_set)
     True
     >>> gnome =  product_set.forReview(search_text='gnome')
     >>> for product in gnome:
@@ -633,9 +634,9 @@
     >>> project = factory.makeProject(name='cat')
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', project_set)
-    True
-    >>> check_permission('launchpad.ProjectReview', project)
-    True
-    >>> check_permission('launchpad.ProjectReview', product)
+    >>> check_permission('launchpad.Moderate', project_set)
+    True
+    >>> check_permission('launchpad.Moderate', project)
+    True
+    >>> check_permission('launchpad.Moderate', product)
     True

=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt	2010-07-28 12:59:06 +0000
+++ lib/lp/registry/doc/vocabularies.txt	2010-08-16 19:36:15 +0000
@@ -1389,7 +1389,7 @@
     Mega Money Maker
 
 If the user is a member of Registry Experts, and therefore has
-launchpad.ProjectReview permission on IProductSet, then all commercial
+launchpad.Moderate permission on IProductSet, then all commercial
 projects are returned.
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
@@ -1402,7 +1402,7 @@
     >>> logout()
 
     >>> login_person(registry_member)
-    >>> check_permission('launchpad.ProjectReview', product_set)
+    >>> check_permission('launchpad.Moderate', product_set)
     True
 
     >>> comm_proj_vocab = get_naked_vocab(registry_member,

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2010-08-12 19:35:11 +0000
+++ lib/lp/registry/interfaces/product.py	2010-08-16 19:36:15 +0000
@@ -10,7 +10,7 @@
 __all__ = [
     'InvalidProductName',
     'IProduct',
-    'IProductProjectReviewRestricted',
+    'IProductModerateRestricted',
     'IProductDriverRestricted',
     'IProductEditRestricted',
     'IProductPublic',
@@ -300,8 +300,8 @@
     """`IProduct` properties which require launchpad.Edit permission."""
 
 
-class IProductProjectReviewRestricted(Interface):
-    """`IProduct` properties which require launchpad.ProjectReview."""
+class IProductModerateRestricted(Interface):
+    """`IProduct` properties which require launchpad.Moderate."""
 
     qualifies_for_free_hosting = exported(
         Bool(
@@ -732,7 +732,7 @@
 
 class IProduct(
     IHasBugSupervisor, IProductEditRestricted,
-    IProductProjectReviewRestricted, IProductDriverRestricted,
+    IProductModerateRestricted, IProductDriverRestricted,
     IProductPublic, IRootContext, IStructuralSubscriptionTarget):
     """A Product.
 

=== added file 'lib/lp/registry/stories/distroseries/distroseries-admin.txt'
--- lib/lp/registry/stories/distroseries/distroseries-admin.txt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/stories/distroseries/distroseries-admin.txt	2010-08-16 19:36:15 +0000
@@ -0,0 +1,89 @@
+Distribution series administration
+=================================
+
+Administrators
+--------------
+
+Launchpad administrators can edit distroseries via two different
+pages: 'Change details' and 'Administer'.
+
+    >>> admin_browser.open('http://launchpad.dev/ubuntu/hoary')
+    >>> print admin_browser.title
+    Hoary (5.04)...
+    >>> admin_browser.getLink('Change details').click()
+    >>> print admin_browser.url
+    http://launchpad.dev/ubuntu/hoary/+edit
+
+    >>> print admin_browser.title
+    Edit The Hoary Hedgehog Release...
+
+    >>> admin_browser.getControl(
+    ...     'Display name', index=0).value = 'Happy'
+    >>> admin_browser.getControl('Change').click()
+    >>> print admin_browser.title
+    Happy (5.04)...
+
+A separate administration page is available via the 'Administer' link.
+
+    >>> admin_browser.open('http://launchpad.dev/ubuntu/hoary')
+    >>> admin_browser.getLink('Administer').click()
+    >>> print admin_browser.url
+    http://launchpad.dev/ubuntu/hoary/+admin
+
+    >>> print admin_browser.title
+    Administer The Hoary Hedgehog Release...
+
+    >>> admin_browser.getControl(
+    ...     'Name', index=0).value = 'happy'
+    >>> admin_browser.getControl(
+    ...     'Version', index=0).value = '5.05'
+    >>> admin_browser.getControl('Change').click()
+    >>> print admin_browser.url
+    http://launchpad.dev/ubuntu/happy
+    >>> print admin_browser.title
+    Happy (5.05)...
+
+
+Registry experts
+----------------
+
+Registry experts do not have access to the 'Change details' link.
+
+    >>> email = "expert@xxxxxxxxxxx"
+    >>> password = "test"
+    >>> registry= factory.makeRegistryExpert(email=email, password=password)
+    >>> logout()
+    >>> registry_browser = setupBrowser(
+    ...     auth='Basic %s:%s' % (email, password))
+    >>> registry_browser.open('http://launchpad.dev/ubuntu/happy')
+    >>> registry_browser.getLink('Change details').click()
+    Traceback (most recent call last):
+    ...
+    LinkNotFoundError
+
+And navigating directly to +edit is thwarted.
+
+    >>> registry_browser.open('http://launchpad.dev/ubuntu/happy/+edit')
+    Traceback (most recent call last):
+    ...
+    Unauthorized...
+
+Registry experts do have access to the administration page.
+
+    >>> registry_browser.open('http://launchpad.dev/ubuntu/happy')
+    >>> registry_browser.getLink('Administer').click()
+    >>> print registry_browser.url
+    http://launchpad.dev/ubuntu/happy/+admin
+
+    >>> print registry_browser.title
+    Administer The Hoary Hedgehog Release...
+
+    >>> registry_browser.getControl(
+    ...     'Name', index=0).value = 'hoary'
+    >>> registry_browser.getControl(
+    ...     'Version', index=0).value = '5.04'
+    >>> registry_browser.getControl('Change').click()
+    >>> print registry_browser.url
+    http://launchpad.dev/ubuntu/hoary
+    >>> print registry_browser.title
+    Happy (5.04)...

=== modified file 'lib/lp/registry/stories/person/xx-admin-person-review.txt'
--- lib/lp/registry/stories/person/xx-admin-person-review.txt	2010-03-31 18:51:32 +0000
+++ lib/lp/registry/stories/person/xx-admin-person-review.txt	2010-08-16 19:36:15 +0000
@@ -91,25 +91,24 @@
     The user is reactivated. He must use the "forgot password" to log in.
 
 
-Partial access for registry experts
------------------------------------
-
-Members of the registry team get partial access to the review account page to
-be able to suspend spam accounts.
-
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> registry_expert = factory.makePerson(email='expert@xxxxxxxxxxx',
-    ...                                      password='test')
-    >>> from canonical.launchpad.interfaces import ILaunchpadCelebrities
-    >>> from zope.component import getUtility
-    >>> registry_team = getUtility(ILaunchpadCelebrities).registry_experts
-    >>> ignored = registry_team.addMember(
-    ...     registry_expert, registry_team.teamowner)
-    >>> print registry_expert.inTeam(registry_team)
-    True
+Registry experts
+----------------
+
+Registry experts can see the +review page in full, just like Launchpad
+admins.
+
+    >>> email = "expert@xxxxxxxxxxx"
+    >>> password = "test"
+    >>> expert= factory.makeRegistryExpert(email=email, password=password)
+    >>> expert_browser = setupBrowser(auth='Basic %s:%s' % (email, password))
     >>> logout()
-
-    >>> expert_browser = setupBrowser(auth='Basic expert@xxxxxxxxxxx:test')
+    >>> expert_browser.open('http://launchpad.dev/~no-priv/+review')
+    >>> print expert_browser.title
+    Review person...
+
+However, members of the registry team only get partial access to the
+review account page to be able to suspend spam accounts.
+
     >>> expert_browser.open('http://launchpad.dev/~no-priv/+reviewaccount')
     >>> print expert_browser.title
     Review person's account...
@@ -163,4 +162,3 @@
     Traceback (most recent call last):
     ...
     Unauthorized: ...
-

=== modified file 'lib/lp/registry/stories/product/xx-product-edit.txt'
--- lib/lp/registry/stories/product/xx-product-edit.txt	2010-06-16 18:49:47 +0000
+++ lib/lp/registry/stories/product/xx-product-edit.txt	2010-08-16 19:36:15 +0000
@@ -235,25 +235,22 @@
     ...
     Unauthorized:...
 
-Even if we add them to the Registry Experts team:
+Tf we add them to the Registry Experts team:
 
     >>> admin_browser.open("http://launchpad.dev/~registry/+addmember";)
     >>> admin_browser.getControl('New member').value = 'no-priv'
     >>> admin_browser.getControl('Add Member').click()
 
-They still cannot edit projects.
+They cannot edit projects.
 
     >>> browser.open('http://launchpad.dev/firefox/+edit')
     Traceback (most recent call last):
     ...
     Unauthorized:...
 
-And they still can't access +admin.
+But they still can access +admin.
 
     >>> browser.open('http://launchpad.dev/firefox/+admin')
-    Traceback (most recent call last):
-    ...
-    Unauthorized...
 
 
 Display error when trying to remove all licenses

=== modified file 'lib/lp/registry/stories/project/xx-project-edit.txt'
--- lib/lp/registry/stories/project/xx-project-edit.txt	2010-06-17 17:10:55 +0000
+++ lib/lp/registry/stories/project/xx-project-edit.txt	2010-08-16 19:36:15 +0000
@@ -19,8 +19,8 @@
     >>> browser.getControl(name='field.bugtracker').value = 'mozilla.org'
     >>> browser.getControl('Change Details').click()
 
-    >>> browser.url
-    'http://launchpad.dev/new-name'
+    >>> print browser.url
+    http://launchpad.dev/new-name
 
 Regular users can't access the +review page.
 
@@ -34,8 +34,8 @@
     >>> admin_browser.open('http://launchpad.dev/new-name')
     >>> admin_browser.getLink('Administer').click()
 
-    >>> admin_browser.url
-    'http://launchpad.dev/new-name/+review'
+    >>> print admin_browser.url
+    http://launchpad.dev/new-name/+review
 
     >>> print admin_browser.title
     Review upstream project group...
@@ -45,8 +45,8 @@
     >>> admin_browser.getControl('Reviewed').selected = True
     >>> admin_browser.getControl('Change').click()
 
-    >>> admin_browser.url
-    'http://launchpad.dev/new-name'
+    >>> print admin_browser.url
+    http://launchpad.dev/new-name
 
 The project summary shows the status as reviewed for admins only.
 
@@ -78,8 +78,8 @@
     >>> admin_browser.getControl('Change').click()
 
     >>> admin_browser.getLink('Administer').click()
-    >>> admin_browser.getControl('Aliases').value
-    'old-name'
+    >>> print admin_browser.getControl('Aliases').value
+    old-name
 
     >>> admin_browser.goBack()
 
@@ -106,3 +106,35 @@
     >>> print extract_text(
     ...     find_tag_by_id(admin_browser.contents, 'registration'))
     Registered ... by Registry Administrators
+
+Registry experts
+----------------
+
+Registry experts are not allowed access to the +edit page.
+
+    >>> email = "expert@xxxxxxxxxxx"
+    >>> password = "test"
+    >>> registry_expert= factory.makeRegistryExpert(email=email,
+    ...                                             password=password)
+    >>> logout()
+    >>> expert_browser = setupBrowser(auth='Basic %s:%s' % (email, password))
+
+    >>> expert_browser.open('http://launchpad.dev/new-name')
+    >>> expert_browser.getLink('Change details').click()
+    Traceback (most recent call last):
+    ...
+    LinkNotFoundError
+
+And going directly to the URL is not allowed.
+
+    >>> expert_browser.open('http://launchpad.dev/new-name/+edit')
+    Traceback (most recent call last):
+    ...
+    Unauthorized...
+
+Registry experts do have full access to administer project groups.
+
+    >>> expert_browser.open('http://launchpad.dev/new-name')
+    >>> expert_browser.getLink('Administer').click()
+    >>> print expert_browser.url
+    http://launchpad.dev/new-name/+review

=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt	2010-07-26 20:18:01 +0000
+++ lib/lp/registry/templates/product-index.pt	2010-08-16 19:36:15 +0000
@@ -160,7 +160,7 @@
             </p>
 
             <div id="project-admin-whiteboard" class="whiteboard"
-              tal:condition="context/required:launchpad.ProjectReview">
+              tal:condition="context/required:launchpad.Moderate">
               <div>
                 <strong>Whiteboard</strong>
                 <a tal:replace="structure overview_menu/review_license/fmt:icon"/>

=== modified file 'lib/lp/registry/templates/project-listing-detailed.pt'
--- lib/lp/registry/templates/project-listing-detailed.pt	2009-08-16 21:14:53 +0000
+++ lib/lp/registry/templates/project-listing-detailed.pt	2010-08-16 19:36:15 +0000
@@ -5,7 +5,7 @@
   omit-tag="">
 
 <a tal:replace="structure context/fmt:link" />
-<tal:review tal:condition="context/required:launchpad.ProjectReview">
+<tal:review tal:condition="context/required:launchpad.Moderate">
   (<a tal:attributes="href context/fmt:url/+review">Review</a>)
 </tal:review>
 

=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py	2010-07-29 20:16:23 +0000
+++ lib/lp/registry/vocabularies.py	2010-08-16 19:36:15 +0000
@@ -1250,7 +1250,7 @@
     A commercial project is one that does not qualify for free hosting.  For
     normal users only commercial projects for which the user is the
     maintainer, or in the maintainers team, will be listed.  For users with
-    launchpad.ProjectReview permission, all commercial projects are returned.
+    launchpad.Moderate permission, all commercial projects are returned.
     """
 
     implements(IHugeVocabulary)
@@ -1279,7 +1279,7 @@
         if user is None:
             return self.emptySelectResults()
         product_set = getUtility(IProductSet)
-        if check_permission('launchpad.ProjectReview', product_set):
+        if check_permission('launchpad.Moderate', product_set):
             projects = product_set.forReview(
                 search_text=query, licenses=[License.OTHER_PROPRIETARY],
                 active=True)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-08-13 14:45:31 +0000
+++ lib/lp/testing/factory.py	2010-08-16 19:36:15 +0000
@@ -28,7 +28,11 @@
 from email.mime.multipart import MIMEMultipart
 from itertools import count
 from operator import isSequenceType
+<<<<<<< TREE
 import os.path
+=======
+import os
+>>>>>>> MERGE-SOURCE
 from random import randint
 from StringIO import StringIO
 from textwrap import dedent
@@ -352,6 +356,18 @@
         login_as(person)
         return person
 
+    def makeRegistryExpert(self, name=None, email='expert@xxxxxxxxxxx',
+                           password='test'):
+        from lp.testing.sampledata import ADMIN_EMAIL
+        login(ADMIN_EMAIL)
+        user = self.makePerson(name=name,
+                               email=email,
+                               password=password)
+        registry_team = getUtility(ILaunchpadCelebrities).registry_experts
+        registry_team.addMember(user, registry_team.teamowner)
+        return user
+
+
     def makeCopyArchiveLocation(self, distribution=None, owner=None,
         name=None, enabled=True):
         """Create and return a new arbitrary location for copy packages."""

=== modified file 'lib/lp/testing/sampledata.py'
--- lib/lp/testing/sampledata.py	2010-08-05 02:29:25 +0000
+++ lib/lp/testing/sampledata.py	2010-08-16 19:36:15 +0000
@@ -11,6 +11,7 @@
 __all__ = [
     'BUILDD_ADMIN_USERNAME',
     'CHROOT_LIBRARYFILEALIAS',
+    'ADMIN_EMAIL',
     'COMMERCIAL_ADMIN_EMAIL',
     'HOARY_DISTROSERIES_NAME',
     'I386_ARCHITECTURE_NAME',
@@ -20,7 +21,6 @@
     'UBUNTU_DEVELOPER_ADMIN_NAME',
     'UBUNTU_DISTRIBUTION_NAME',
     'UBUNTU_UPLOAD_TEAM_NAME',
-    'UBUNTUTEST_DISTRIBUTION_NAME',
     'WARTY_DISTROSERIES_NAME',
     'WARTY_ONLY_SOURCEPACKAGENAME',
     'WARTY_ONLY_SOURCEPACKAGEVERSION',
@@ -42,6 +42,8 @@
 MAIN_COMPONENT_NAME = 'main'
 NO_PRIVILEGE_EMAIL = 'no-priv@xxxxxxxxxxxxx'
 COMMERCIAL_ADMIN_EMAIL = 'commercial-member@xxxxxxxxxxxxx'
+ADMIN_EMAIL = 'foo.bar@xxxxxxxxxxxxx'
+
 # A user that is an admin of ubuntu-team, which has upload rights
 # to Ubuntu.
 UBUNTU_DEVELOPER_ADMIN_NAME = 'name16'