Merge lp:~bac/launchpad/bug-662994 into lp:launchpad

Proposed by Brad Crittenden on 2010-12-14
Status: Merged
Approved by: Graham Binns on 2010-12-14
Approved revision: no longer in the source branch.
Merged at revision: 12076
Proposed branch: lp:~bac/launchpad/bug-662994
Merge into: lp:launchpad
Diff against target: 410 lines (+176/-68)
7 files modified
lib/lp/registry/browser/project.py (+5/-3)
lib/lp/registry/browser/tests/test_projectgroup.py (+84/-0)
lib/lp/registry/interfaces/projectgroup.py (+9/-10)
lib/lp/registry/stories/object/xx-nameblacklist.txt (+38/-41)
lib/lp/registry/stories/project/xx-project-edit.txt (+8/-11)
lib/lp/registry/tests/test_projectgroup.py (+30/-2)
lib/lp/testing/matchers.py (+2/-1)
To merge this branch: bzr merge lp:~bac/launchpad/bug-662994
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2010-12-14 Approve on 2010-12-14
Review via email: mp+43647@code.launchpad.net

Commit Message

[r=gmb][ui=none][bug=662994] Allow registry experts to change project group name.

Description of the Change

= Summary =

A task for CHR and Curtis is to change project group names. The
registry team has never had the ability to do so.

== Proposed fix ==

Move the permission of name from launchpad.Edit to launchpad.Moderate
and from the +edit to the +review page.

== Pre-implementation notes ==

Talk with Curtis

== Implementation details ==

The change described gives ~registry the ability to change a PG name but
removes it from the PG owner. This effect is necessary due to the way
our permissions are partitioned between owners, admins, and registry.
On a positive note, it brings behavior of PG editing in-line with projects.

I also changed the matchers to expose 'Contains' in __all__ which had
been inadvertently left out.

== Tests ==

bin/test -vvm lp.registry -t xx-project-edit.txt -t test_projectgroup

== Demo and Q/A ==

= Launchpad lint =

I will fix these lint issues.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/stories/project/xx-project-edit.txt
  lib/lp/registry/tests/test_projectgroup.py
  lib/lp/registry/stories/object/xx-nameblacklist.txt
  lib/lp/registry/interfaces/projectgroup.py
  lib/lp/registry/browser/tests/test_projectgroup.py
  lib/lp/testing/matchers.py
  lib/lp/registry/browser/project.py

./lib/lp/registry/stories/object/xx-nameblacklist.txt
       7: source has bad indentation.
      14: source exceeds 78 characters.
      20: source has bad indentation.
      24: source exceeds 78 characters.
      30: source has bad indentation.
      39: source has bad indentation.
      56: source has bad indentation.
      61: source has bad indentation.
./lib/lp/testing/matchers.py
     144: E231 missing whitespace after ':'
     236: Line exceeds 78 characters.

To post a comment you must log in.
Graham Binns (gmb) wrote :

Hi Brad,

Just a few stylistic nitpicks; nothing major.

74 + view = create_initialized_view(self.project_group, '+index',
75 + principal=user)

Arguments should start on the line after the opening parenthesis,
indented by 4 spaces.

85 + view = create_initialized_view(self.project_group, '+index',
86 + principal=user)

Here too.

95 + view = create_initialized_view(self.project_group, '+index',
96 + principal=user)

And here.

290 + self.assertRaises(Unauthorized,
291 + setattr, self.pg, 'owner', new_owner)

And here.

295 + self.assertRaises(Unauthorized,
296 + setattr, self.pg, 'name', 'new-name')

And here.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/project.py'
2--- lib/lp/registry/browser/project.py 2010-12-01 04:27:44 +0000
3+++ lib/lp/registry/browser/project.py 2010-12-14 19:21:42 +0000
4@@ -350,11 +350,11 @@
5 class ProjectEditView(LaunchpadEditFormView):
6 """View class that lets you edit a Project object."""
7 implements(IProjectGroupEditMenu)
8-
9 label = "Change project group details"
10+ page_title = label
11 schema = IProjectGroup
12 field_names = [
13- 'name', 'displayname', 'title', 'summary', 'description',
14+ 'displayname', 'title', 'summary', 'description',
15 'bug_reporting_guidelines', 'bug_reported_acknowledgement',
16 'homepageurl', 'bugtracker', 'sourceforgeproject',
17 'freshmeatproject', 'wikiurl']
18@@ -388,8 +388,10 @@
19 self.field_names = self.default_field_names[:]
20 admin = check_permission('launchpad.Admin', self.context)
21 if not admin:
22+ self.field_names.remove('owner')
23+ moderator = check_permission('launchpad.Moderate', self.context)
24+ if not moderator:
25 self.field_names.remove('name')
26- self.field_names.remove('owner')
27 super(ProjectReviewView, self).setUpFields()
28 self.form_fields = self._createAliasesField() + self.form_fields
29 if admin:
30
31=== added file 'lib/lp/registry/browser/tests/test_projectgroup.py'
32--- lib/lp/registry/browser/tests/test_projectgroup.py 1970-01-01 00:00:00 +0000
33+++ lib/lp/registry/browser/tests/test_projectgroup.py 2010-12-14 19:21:42 +0000
34@@ -0,0 +1,84 @@
35+# Copyright 2010 Canonical Ltd. This software is licensed under the
36+# GNU Affero General Public License version 3 (see the file LICENSE).
37+
38+"""Tests for project group views."""
39+
40+__metaclass__ = type
41+
42+from zope.component import getUtility
43+from zope.security.interfaces import Unauthorized
44+from testtools.matchers import Not
45+from canonical.launchpad.webapp import canonical_url
46+from canonical.launchpad.webapp.interfaces import ILaunchBag
47+from canonical.testing.layers import DatabaseFunctionalLayer
48+from lp.registry.interfaces.person import IPersonSet
49+from lp.testing import (
50+ celebrity_logged_in,
51+ person_logged_in,
52+ TestCaseWithFactory,
53+ )
54+from lp.testing.matchers import (
55+ Contains,
56+ )
57+from lp.testing.sampledata import ADMIN_EMAIL
58+from lp.testing.views import create_initialized_view
59+
60+
61+class TestProjectGroupEditView(TestCaseWithFactory):
62+ """Tests the edit view."""
63+
64+ layer = DatabaseFunctionalLayer
65+
66+ def setUp(self):
67+ super(TestProjectGroupEditView, self).setUp()
68+ self.project_group = self.factory.makeProject(name='grupo')
69+
70+ def test_links_admin(self):
71+ # An admin can change details and administer a project group.
72+ with celebrity_logged_in('admin'):
73+ user = getUtility(ILaunchBag).user
74+ view = create_initialized_view(
75+ self.project_group, '+index', principal=user)
76+ contents = view.render()
77+ self.assertThat(contents, Contains("Change details"))
78+ self.assertThat(contents, Contains("Administer"))
79+
80+ def test_links_registry_expert(self):
81+ # A registry expert cannot change details but can administer a project
82+ # group.
83+ with celebrity_logged_in('registry_experts'):
84+ user = getUtility(ILaunchBag).user
85+ view = create_initialized_view(
86+ self.project_group, '+index', principal=user)
87+ contents = view.render()
88+ self.assertThat(contents, Not(Contains("Change details")))
89+ self.assertThat(contents, Contains("Administer"))
90+
91+ def test_links_owner(self):
92+ # An owner can change details but not administer a project group.
93+ with person_logged_in(self.project_group.owner):
94+ user = getUtility(ILaunchBag).user
95+ view = create_initialized_view(
96+ self.project_group, '+index', principal=user)
97+ contents = view.render()
98+ self.assertThat(contents, Contains("Change details"))
99+ self.assertThat(contents, Not(Contains("Administer")))
100+
101+ def test_edit_view_admin(self):
102+ admin = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
103+ browser = self.getUserBrowser(user=admin)
104+ browser.open(canonical_url(self.project_group, view_name='+edit'))
105+ browser.open(canonical_url(self.project_group, view_name='+review'))
106+
107+ def test_edit_view_registry_expert(self):
108+ registry_expert = self.factory.makeRegistryExpert()
109+ browser = self.getUserBrowser(user=registry_expert)
110+ url = canonical_url(self.project_group, view_name='+edit')
111+ self.assertRaises(Unauthorized, browser.open, url)
112+ browser.open(canonical_url(self.project_group, view_name='+review'))
113+
114+ def test_edit_view_owner(self):
115+ browser = self.getUserBrowser(user=self.project_group.owner)
116+ browser.open(canonical_url(self.project_group, view_name='+edit'))
117+ url = canonical_url(self.project_group, view_name='+review')
118+ self.assertRaises(Unauthorized, browser.open, url)
119
120=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
121--- lib/lp/registry/interfaces/projectgroup.py 2010-12-01 20:53:19 +0000
122+++ lib/lp/registry/interfaces/projectgroup.py 2010-12-14 19:21:42 +0000
123@@ -104,6 +104,15 @@
124 title=_('Reviewed'), required=False,
125 description=_("Whether or not this project group has been "
126 "reviewed.")))
127+ name = exported(
128+ ProjectNameField(
129+ title=_('Name'),
130+ required=True,
131+ description=_(
132+ "A unique name, used in URLs, identifying the project "
133+ "group. All lowercase, no special characters. "
134+ "Examples: apache, mozilla, gimp."),
135+ constraint=name_validator))
136
137
138 class IProjectGroupPublic(
139@@ -133,16 +142,6 @@
140 description=_("Project group registrant. Must be a valid "
141 "Launchpad Person.")))
142
143- name = exported(
144- ProjectNameField(
145- title=_('Name'),
146- required=True,
147- description=_(
148- "A unique name, used in URLs, identifying the project "
149- "group. All lowercase, no special characters. "
150- "Examples: apache, mozilla, gimp."),
151- constraint=name_validator))
152-
153 displayname = exported(
154 TextLine(
155 title=_('Display Name'),
156
157=== modified file 'lib/lp/registry/stories/object/xx-nameblacklist.txt'
158--- lib/lp/registry/stories/object/xx-nameblacklist.txt 2010-11-03 20:38:32 +0000
159+++ lib/lp/registry/stories/object/xx-nameblacklist.txt 2010-12-14 19:21:42 +0000
160@@ -4,44 +4,42 @@
161
162 Try creating a project with a blacklisted name:
163
164->>> admin_browser.open('http://launchpad.dev/projectgroups/+new')
165->>> admin_browser.getControl('Name', index=0).value = 'blacklisted'
166->>> admin_browser.getControl('Display Name').value = 'Whatever'
167->>> admin_browser.getControl('Title').value = 'Whatever'
168->>> admin_browser.getControl('Project Group Summary').value = 'Whatever'
169->>> admin_browser.getControl('Description').value = 'Whatever'
170->>> admin_browser.getControl('Add').click()
171->>> "The name 'blacklisted' has been blocked by the Launchpad administrators" \
172-... in admin_browser.contents
173-True
174+ >>> admin_browser.open('http://launchpad.dev/projectgroups/+new')
175+ >>> admin_browser.getControl('Name', index=0).value = 'blacklisted'
176+ >>> admin_browser.getControl('Display Name').value = 'Whatever'
177+ >>> admin_browser.getControl('Title').value = 'Whatever'
178+ >>> admin_browser.getControl('Project Group Summary').value = 'Whatever'
179+ >>> admin_browser.getControl('Description').value = 'Whatever'
180+ >>> admin_browser.getControl('Add').click()
181+ >>> "The name 'blacklisted' has been blocked" in admin_browser.contents
182+ True
183
184 Try renaming a project to a blacklisted name:
185
186->>> admin_browser.open('http://launchpad.dev/mozilla')
187->>> admin_browser.getLink('Change details').click()
188->>> admin_browser.getControl('Name', index=0).value = 'blacklisted'
189->>> admin_browser.getControl('Change Details').click()
190->>> "The name 'blacklisted' has been blocked by the Launchpad administrators" \
191-... in admin_browser.contents
192-True
193+ >>> admin_browser.open('http://launchpad.dev/mozilla')
194+ >>> admin_browser.getLink('Administer').click()
195+ >>> admin_browser.getControl('Name', index=0).value = 'blacklisted'
196+ >>> admin_browser.getControl('Change Details').click()
197+ >>> "The name 'blacklisted' has been blocked" in admin_browser.contents
198+ True
199
200 Same behavior for products:
201
202->>> admin_browser.open('http://launchpad.dev/firefox')
203->>> admin_browser.getLink('Administer').click()
204->>> admin_browser.getControl('Name').value = 'blacklisted'
205->>> admin_browser.getControl('Change').click()
206->>> "The name 'blacklisted' has been blocked" in admin_browser.contents
207-True
208+ >>> admin_browser.open('http://launchpad.dev/firefox')
209+ >>> admin_browser.getLink('Administer').click()
210+ >>> admin_browser.getControl('Name').value = 'blacklisted'
211+ >>> admin_browser.getControl('Change').click()
212+ >>> "The name 'blacklisted' has been blocked" in admin_browser.contents
213+ True
214
215 Same behavior for people:
216
217->>> admin_browser.open('http://launchpad.dev/~stub')
218->>> admin_browser.getLink('Change details').click()
219->>> admin_browser.getControl('Name', index=1).value = 'admin42'
220->>> admin_browser.getControl('Save').click()
221->>> "The name 'admin42' has been blocked" in admin_browser.contents
222-True
223+ >>> admin_browser.open('http://launchpad.dev/~stub')
224+ >>> admin_browser.getLink('Change details').click()
225+ >>> admin_browser.getControl('Name', index=1).value = 'admin42'
226+ >>> admin_browser.getControl('Save').click()
227+ >>> "The name 'admin42' has been blocked" in admin_browser.contents
228+ True
229
230 Note that it is possible to have an object with a blacklisted name. These
231 objects were either created before the blacklist was implemented, or have
232@@ -51,15 +49,14 @@
233 be a Launchpad Celebrity.
234
235 We can edit the details of an object with a blacklisted name quite
236-happily without generating
237-
238->>> admin_browser.open('http://launchpad.dev/~admins')
239->>> admin_browser.getLink('Change details').click()
240->>> admin_browser.getControl('Display Name').value = 'Different'
241->>> admin_browser.getControl('Save').click()
242-
243->>> print get_feedback_messages(admin_browser.contents)
244-[]
245->>> "has been blocked" in admin_browser.contents
246-False
247-
248+happily without generating
249+
250+ >>> admin_browser.open('http://launchpad.dev/~admins')
251+ >>> admin_browser.getLink('Change details').click()
252+ >>> admin_browser.getControl('Display Name').value = 'Different'
253+ >>> admin_browser.getControl('Save').click()
254+
255+ >>> print get_feedback_messages(admin_browser.contents)
256+ []
257+ >>> "has been blocked" in admin_browser.contents
258+ False
259
260=== modified file 'lib/lp/registry/stories/project/xx-project-edit.txt'
261--- lib/lp/registry/stories/project/xx-project-edit.txt 2010-08-20 18:13:20 +0000
262+++ lib/lp/registry/stories/project/xx-project-edit.txt 2010-12-14 19:21:42 +0000
263@@ -10,7 +10,6 @@
264 Change project group details...
265
266 >>> soup = find_main_content(browser.contents)
267- >>> browser.getControl(name='field.name').value = 'new-name'
268 >>> browser.getControl('Display Name').value = 'New Name'
269 >>> browser.getControl('Title').value = 'New Title'
270 >>> browser.getControl('Project Group Summary').value = 'New Summary.'
271@@ -20,7 +19,7 @@
272 >>> browser.getControl('Change Details').click()
273
274 >>> print browser.url
275- http://launchpad.dev/new-name
276+ http://launchpad.dev/gnome
277
278 Regular users can't access the +review page.
279
280@@ -31,18 +30,19 @@
281
282 But administrators can access the page:
283
284- >>> admin_browser.open('http://launchpad.dev/new-name')
285+ >>> admin_browser.open('http://launchpad.dev/gnome')
286 >>> admin_browser.getLink('Administer').click()
287
288 >>> print admin_browser.url
289- http://launchpad.dev/new-name/+review
290+ http://launchpad.dev/gnome/+review
291
292 >>> print admin_browser.title
293- Review upstream project group...
294+ Change project group details : New Name
295
296-Mark the project as reviewed.
297+Mark the project as reviewed and change the name.
298
299 >>> admin_browser.getControl('Reviewed').selected = True
300+ >>> admin_browser.getControl(name='field.name').value = 'new-name'
301 >>> admin_browser.getControl('Change').click()
302
303 >>> print admin_browser.url
304@@ -148,17 +148,14 @@
305 Traceback (most recent call last):
306 ...
307 LookupError: label 'Registrant'
308- >>> expert_browser.getControl('Name')
309- Traceback (most recent call last):
310- ...
311- LookupError: label 'Name'
312
313+ >>> expert_browser.getControl('Name').value = 'newer-name'
314 >>> expert_browser.getControl('Aliases').value = 'sleepy'
315 >>> expert_browser.getControl('Active').selected = False
316 >>> expert_browser.getControl('Reviewed').selected = False
317 >>> expert_browser.getControl('Change').click()
318
319- >>> expert_browser.open('http://launchpad.dev/new-name')
320+ >>> expert_browser.open('http://launchpad.dev/newer-name')
321 >>> expert_browser.getLink('Administer').click()
322 >>> print expert_browser.getControl('Aliases').value
323 sleepy
324
325=== renamed file 'lib/lp/registry/tests/test_project.py' => 'lib/lp/registry/tests/test_projectgroup.py'
326--- lib/lp/registry/tests/test_project.py 2010-12-08 17:22:23 +0000
327+++ lib/lp/registry/tests/test_projectgroup.py 2010-12-14 19:21:42 +0000
328@@ -7,8 +7,8 @@
329
330 from lazr.restfulclient.errors import ClientError
331 from zope.component import getUtility
332+from zope.security.interfaces import Unauthorized
333
334-from canonical.launchpad.ftests import login
335 from canonical.launchpad.webapp.errorlog import globalErrorUtility
336 from canonical.testing.layers import (
337 DatabaseFunctionalLayer,
338@@ -17,6 +17,7 @@
339 from lp.registry.interfaces.projectgroup import IProjectGroupSet
340 from lp.testing import (
341 launchpadlib_for,
342+ login_celebrity,
343 login_person,
344 TestCaseWithFactory,
345 )
346@@ -108,6 +109,33 @@
347 self.assertEqual(self.project3, results[0])
348
349
350+class TestProjectGroupPermissions(TestCaseWithFactory):
351+
352+ layer = DatabaseFunctionalLayer
353+
354+ def setUp(self):
355+ super(TestProjectGroupPermissions, self).setUp()
356+ self.pg = self.factory.makeProject(name='my-project-group')
357+
358+ def test_attribute_changes_by_admin(self):
359+ login_celebrity('admin')
360+ self.pg.name = 'new-name'
361+ self.pg.owner = self.factory.makePerson(name='project-group-owner')
362+
363+ def test_attribute_changes_by_registry_admin(self):
364+ login_celebrity('registry_experts')
365+ new_owner = self.factory.makePerson(name='project-group-owner')
366+ self.pg.name = 'new-name'
367+ self.assertRaises(
368+ Unauthorized, setattr, self.pg, 'owner', new_owner)
369+
370+ def test_attribute_changes_by_owner(self):
371+ login_person(self.pg.owner)
372+ self.assertRaises(
373+ Unauthorized, setattr, self.pg, 'name', 'new-name')
374+ self.pg.owner = self.factory.makePerson(name='project-group-owner')
375+
376+
377 class TestLaunchpadlibAPI(TestCaseWithFactory):
378
379 layer = DatabaseFunctionalLayer
380@@ -117,8 +145,8 @@
381 # is raised when trying to deactivate a project that has source
382 # releases.
383 last_oops = globalErrorUtility.getLastOopsReport()
384+
385 launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
386-
387 project = launchpad.projects['evolution']
388 project.active = False
389 e = self.assertRaises(ClientError, project.lp_save)
390
391=== modified file 'lib/lp/testing/matchers.py'
392--- lib/lp/testing/matchers.py 2010-10-19 22:28:51 +0000
393+++ lib/lp/testing/matchers.py 2010-12-14 19:21:42 +0000
394@@ -3,6 +3,7 @@
395
396 __metaclass__ = type
397 __all__ = [
398+ 'Contains',
399 'DoesNotCorrectlyProvide',
400 'DoesNotProvide',
401 'HasQueryCount',
402@@ -141,7 +142,7 @@
403 for query in self.query_collector.queries:
404 result.append(unicode(query).encode('utf8'))
405 return {'queries': Content(UTF8_TEXT, lambda:['\n'.join(result)])}
406-
407+
408
409 class IsNotProxied(Mismatch):
410 """An object is not proxied."""