Merge lp:~sinzui/launchpad/mailing-list-name-field into lp:launchpad

Proposed by Curtis Hovey on 2012-01-27
Status: Merged
Approved by: Brad Crittenden on 2012-01-27
Approved revision: no longer in the source branch.
Merged at revision: 14732
Proposed branch: lp:~sinzui/launchpad/mailing-list-name-field
Merge into: lp:launchpad
Diff against target: 423 lines (+151/-42)
5 files modified
lib/lp/registry/browser/configure.zcml (+6/-0)
lib/lp/registry/browser/person.py (+20/-18)
lib/lp/registry/browser/team.py (+7/-0)
lib/lp/registry/browser/tests/test_person.py (+81/-17)
lib/lp/registry/browser/tests/test_team.py (+37/-7)
To merge this branch: bzr merge lp:~sinzui/launchpad/mailing-list-name-field
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code 2012-01-27 Approve on 2012-01-27
Review via email: mp+90479@code.launchpad.net

Description of the Change

iDo not imply a team can be renamed when it has a mailing list.

    Launchpad bug: https://bugs.launchpad.net/bugs/889496
    Pre-implementation: myself

The field to rename was shown implying I could do the rename. The form
did not check that there was an mailing list. I was confused actually,
because the team page did not show an list. This list was being created
:(.

This is not an issue with the +edit page. I was using the +review page.
I think the two view need to share some code.

    * +review implies I can change the name of a user with an active PPA...
      PersonRenameFormMixin. This is broken for users and teams.
    * There are two options. A. subclass TeamEditView or
      create a new class using TeamFormMixin, PersonRenameFormMixin.

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

RULES

    * Extract the rename tests from TestPersonEditView to a mixin.
    * Add a testcase for PersonAdministerView that used the new mixin.
    * Update PersonAdministerView to extend PersonRenameFormMixin
    * Extract the rename tests from TestTeamEditView to a mixin.
    * Add a test for for TeamAdministerView that uses the mixin.
    * Create TeamAdministerView from PersonAdministerViewthat does
      not have standing.

QA

    * Visit https://qastaging.launchpad.net/~test234/+review
    * Verify that the name field is not editable.
    * Visit https://qastaging.launchpad.net/~sinzui/+review
    * Verify that the name field is not editable.

LINT

    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/team.py
    lib/lp/registry/browser/tests/test_person_view.py
    lib/lp/registry/browser/tests/test_team.py

TEST

    ./bin/test -vv -t TestPersonEditView -t PersonAdministerViewTestCase \
        lp.registry.browser.tests.test_person_view
    ./bin/test -vvc -t TestTeamEditView -t TeamAdminisiterViewTestCase \
        lp.registry.browser.tests.test_team

IMPLEMENTATION

Updated PersonAdministerView to use PersonRenameFormMixin. Extracted the
common rename tests to a mixin and used it in both the edit and
administer test cases. I had to move PersonRenameFormMixin which inflated
the diff. I modernised the setup of the mixing tests. I added tests about
which fields are visible to admins and registry_experts because I did not
see the rules verified in other browser tests.
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_person_view.py

Subclassed PersonAdministerView so that I do not see the user standing
fields when looking at a team. Extracted the common rename tests to a mixin
and used it in both the edit and administer test cases.
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/team.py
    lib/lp/registry/browser/tests/test_team.py

To post a comment you must log in.
Brad Crittenden (bac) wrote :

Hi Curtis,

Thanks for fixing this bad UI.

This was my first encounter with checkRename(). I found it confusing that a return of None meant things were a-ok. Since your use is the only call site perhaps a brief comment?

typo: s/do no see/do not see/ (twice)

Otherwise it looks great.

--bac

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/configure.zcml'
2--- lib/lp/registry/browser/configure.zcml 2012-01-18 16:49:09 +0000
3+++ lib/lp/registry/browser/configure.zcml 2012-01-27 17:43:25 +0000
4@@ -1152,6 +1152,12 @@
5 permission="launchpad.Edit"
6 template="../templates/team-edit.pt"/>
7 <browser:page
8+ name="+review"
9+ for="lp.registry.interfaces.person.ITeam"
10+ class="lp.registry.browser.team.TeamAdministerView"
11+ permission="launchpad.Moderate"
12+ template="../templates/person-review.pt"/>
13+ <browser:page
14 name="+branding"
15 for="lp.registry.interfaces.person.ITeam"
16 class="lp.registry.browser.team.TeamBrandingView"
17
18=== modified file 'lib/lp/registry/browser/person.py'
19--- lib/lp/registry/browser/person.py 2012-01-25 04:56:50 +0000
20+++ lib/lp/registry/browser/person.py 2012-01-27 17:43:25 +0000
21@@ -1225,7 +1225,26 @@
22 return encodeddata
23
24
25-class PersonAdministerView(LaunchpadEditFormView):
26+class PersonRenameFormMixin(LaunchpadEditFormView):
27+
28+ def setUpWidgets(self):
29+ """See `LaunchpadViewForm`.
30+
31+ Renames are prohibited if a person/team has an active PPA or an
32+ active mailing list.
33+ """
34+ reason = self.context.checkRename()
35+ # Reason is a message about why a rename cannot happen.
36+ # No message means renames are permitted.
37+ if reason:
38+ # This makes the field's widget display (i.e. read) only.
39+ self.form_fields['name'].for_display = True
40+ super(PersonRenameFormMixin, self).setUpWidgets()
41+ if reason:
42+ self.widgets['name'].hint = reason
43+
44+
45+class PersonAdministerView(PersonRenameFormMixin):
46 """Administer an `IPerson`."""
47 schema = IPerson
48 label = "Review person"
49@@ -3475,23 +3494,6 @@
50 page_title = label
51
52
53-class PersonRenameFormMixin(LaunchpadEditFormView):
54-
55- def setUpWidgets(self):
56- """See `LaunchpadViewForm`.
57-
58- Renames are prohibited if a person/team has an active PPA or an
59- active mailing list.
60- """
61- reason = self.context.checkRename()
62- if reason:
63- # This makes the field's widget display (i.e. read) only.
64- self.form_fields['name'].for_display = True
65- super(PersonRenameFormMixin, self).setUpWidgets()
66- if reason:
67- self.widgets['name'].hint = reason
68-
69-
70 class PersonEditView(PersonRenameFormMixin, BasePersonEditView):
71 """The Person 'Edit' page."""
72
73
74=== modified file 'lib/lp/registry/browser/team.py'
75--- lib/lp/registry/browser/team.py 2012-01-25 04:56:50 +0000
76+++ lib/lp/registry/browser/team.py 2012-01-27 17:43:25 +0000
77@@ -95,6 +95,7 @@
78 from lp.registry.browser.objectreassignment import ObjectReassignmentView
79 from lp.registry.browser.person import (
80 CommonMenuLinks,
81+ PersonAdministerView,
82 PersonIndexView,
83 PersonNavigation,
84 PersonRenameFormMixin,
85@@ -353,6 +354,12 @@
86 cancel_url = next_url
87
88
89+class TeamAdministerView(PersonAdministerView):
90+ """A view to administer teams on behalf of users."""
91+ label = "Review team"
92+ default_field_names = ['name', 'displayname']
93+
94+
95 def generateTokenAndValidationEmail(email, team):
96 """Send a validation message to the given email."""
97 login = getUtility(ILaunchBag).login
98
99=== removed file 'lib/lp/registry/browser/tests/test_person.py'
100--- lib/lp/registry/browser/tests/test_person.py 2012-01-01 02:58:52 +0000
101+++ lib/lp/registry/browser/tests/test_person.py 1970-01-01 00:00:00 +0000
102@@ -1,52 +0,0 @@
103-# Copyright 2009 Canonical Ltd. This software is licensed under the
104-# GNU Affero General Public License version 3 (see the file LICENSE).
105-
106-"""Test harness for person views unit tests."""
107-
108-__metaclass__ = type
109-
110-from textwrap import dedent
111-
112-from lp.registry.browser.person import PersonView
113-from lp.services.config import config
114-from lp.services.webapp.servers import LaunchpadTestRequest
115-from lp.testing import (
116- login_person,
117- TestCaseWithFactory,
118- )
119-from lp.testing.layers import DatabaseFunctionalLayer
120-
121-
122-class PersonView_openid_identity_url_TestCase(TestCaseWithFactory):
123- """Tests for the public OpenID identifier shown on the profile page."""
124-
125- layer = DatabaseFunctionalLayer
126-
127- def setUp(self):
128- TestCaseWithFactory.setUp(self)
129- self.user = self.factory.makePerson(name='eris')
130- self.request = LaunchpadTestRequest(
131- SERVER_URL="http://launchpad.dev/")
132- login_person(self.user, self.request)
133- self.view = PersonView(self.user, self.request)
134- # Marker allowing us to reset the config.
135- config.push(self.id(), '')
136- self.addCleanup(config.pop, self.id())
137-
138- def test_should_be_profile_page_when_delegating(self):
139- """The profile page is the OpenID identifier in normal situation."""
140- self.assertEquals(
141- 'http://launchpad.dev/~eris', self.view.openid_identity_url)
142-
143- def test_should_be_production_profile_page_when_not_delegating(self):
144- """When the profile page is not delegated, the OpenID identity URL
145- should be the one on the main production site."""
146- config.push('non-delegating', dedent('''
147- [vhost.mainsite]
148- openid_delegate_profile: False
149-
150- [launchpad]
151- non_restricted_hostname: prod.launchpad.dev
152- '''))
153- self.assertEquals(
154- 'http://prod.launchpad.dev/~eris', self.view.openid_identity_url)
155
156=== renamed file 'lib/lp/registry/browser/tests/test_person_view.py' => 'lib/lp/registry/browser/tests/test_person.py'
157--- lib/lp/registry/browser/tests/test_person_view.py 2012-01-20 15:42:44 +0000
158+++ lib/lp/registry/browser/tests/test_person.py 2012-01-27 17:43:25 +0000
159@@ -3,6 +3,7 @@
160
161 __metaclass__ = type
162
163+from textwrap import dedent
164 import doctest
165
166 import soupmatchers
167@@ -21,10 +22,7 @@
168 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
169 from lp.bugs.model.bugtask import BugTask
170 from lp.buildmaster.enums import BuildStatus
171-from lp.registry.browser.person import (
172- PersonEditView,
173- PersonView,
174- )
175+from lp.registry.browser.person import PersonView
176 from lp.registry.browser.team import TeamInvitationView
177 from lp.registry.interfaces.karma import IKarmaCacheManager
178 from lp.registry.interfaces.person import (
179@@ -57,6 +55,7 @@
180 from lp.testing import (
181 ANONYMOUS,
182 login,
183+ login_celebrity,
184 login_person,
185 person_logged_in,
186 StormStatementRecorder,
187@@ -79,6 +78,41 @@
188 )
189
190
191+class PersonViewOpenidIdentityUrlTestCase(TestCaseWithFactory):
192+ """Tests for the public OpenID identifier shown on the profile page."""
193+
194+ layer = DatabaseFunctionalLayer
195+
196+ def setUp(self):
197+ TestCaseWithFactory.setUp(self)
198+ self.user = self.factory.makePerson(name='eris')
199+ self.request = LaunchpadTestRequest(
200+ SERVER_URL="http://launchpad.dev/")
201+ login_person(self.user, self.request)
202+ self.view = PersonView(self.user, self.request)
203+ # Marker allowing us to reset the config.
204+ config.push(self.id(), '')
205+ self.addCleanup(config.pop, self.id())
206+
207+ def test_should_be_profile_page_when_delegating(self):
208+ """The profile page is the OpenID identifier in normal situation."""
209+ self.assertEquals(
210+ 'http://launchpad.dev/~eris', self.view.openid_identity_url)
211+
212+ def test_should_be_production_profile_page_when_not_delegating(self):
213+ """When the profile page is not delegated, the OpenID identity URL
214+ should be the one on the main production site."""
215+ config.push('non-delegating', dedent('''
216+ [vhost.mainsite]
217+ openid_delegate_profile: False
218+
219+ [launchpad]
220+ non_restricted_hostname: prod.launchpad.dev
221+ '''))
222+ self.assertEquals(
223+ 'http://prod.launchpad.dev/~eris', self.view.openid_identity_url)
224+
225+
226 class TestPersonIndexView(TestCaseWithFactory):
227
228 layer = DatabaseFunctionalLayer
229@@ -136,7 +170,7 @@
230 def test_private_superteams_anonymous(self):
231 # If the viewer is anonymous, the portlet is not shown.
232 team = self.createTeams()
233- viewer = self.factory.makePerson()
234+ self.factory.makePerson()
235 view = create_initialized_view(
236 team, '+index', server_url=canonical_url(team), path_info='')
237 html = view()
238@@ -324,18 +358,7 @@
239 self.failUnless(person_view.should_show_ppa_section)
240
241
242-class TestPersonEditView(TestCaseWithFactory):
243-
244- layer = LaunchpadFunctionalLayer
245-
246- def setUp(self):
247- TestCaseWithFactory.setUp(self)
248- self.valid_email_address = self.factory.getUniqueEmailAddress()
249- self.person = self.factory.makePerson(email=self.valid_email_address)
250- login_person(self.person)
251- self.ppa = self.factory.makeArchive(owner=self.person)
252- self.view = PersonEditView(
253- self.person, LaunchpadTestRequest())
254+class TestPersonRenameFormMixin:
255
256 def test_can_rename_with_empty_PPA(self):
257 # If a PPA exists but has no packages, we can rename the
258@@ -380,6 +403,19 @@
259 self.view.initialize()
260 self.assertFalse(self.view.form_fields['name'].for_display)
261
262+
263+class TestPersonEditView(TestPersonRenameFormMixin, TestCaseWithFactory):
264+
265+ layer = LaunchpadFunctionalLayer
266+
267+ def setUp(self):
268+ super(TestPersonEditView, self).setUp()
269+ self.valid_email_address = self.factory.getUniqueEmailAddress()
270+ self.person = self.factory.makePerson(email=self.valid_email_address)
271+ login_person(self.person)
272+ self.ppa = self.factory.makeArchive(owner=self.person)
273+ self.view = create_initialized_view(self.person, '+edit')
274+
275 def test_add_email_good_data(self):
276 email_address = self.factory.getUniqueEmailAddress()
277 form = {
278@@ -423,6 +459,34 @@
279 self.assertEqual(expected_msg, error_msg)
280
281
282+class PersonAdministerViewTestCase(TestPersonRenameFormMixin,
283+ TestCaseWithFactory):
284+ layer = LaunchpadFunctionalLayer
285+
286+ def setUp(self):
287+ super(PersonAdministerViewTestCase, self).setUp()
288+ self.person = self.factory.makePerson()
289+ login_celebrity('admin')
290+ self.ppa = self.factory.makeArchive(owner=self.person)
291+ self.view = create_initialized_view(self.person, '+review')
292+
293+ def test_init_admin(self):
294+ # An admin sees all the fields.
295+ self.assertEqual('Review person', self.view.label)
296+ self.assertEqual(
297+ ['name', 'displayname', 'personal_standing',
298+ 'personal_standing_reason'],
299+ self.view.field_names)
300+
301+ def test_init_registry_expert(self):
302+ # Registry experts do not see the the displayname field.
303+ login_celebrity('registry_experts')
304+ self.view.setUpFields()
305+ self.assertEqual(
306+ ['name', 'personal_standing', 'personal_standing_reason'],
307+ self.view.field_names)
308+
309+
310 class TestTeamCreationView(TestCaseWithFactory):
311
312 layer = DatabaseFunctionalLayer
313
314=== modified file 'lib/lp/registry/browser/tests/test_team.py'
315--- lib/lp/registry/browser/tests/test_team.py 2012-01-23 18:15:34 +0000
316+++ lib/lp/registry/browser/tests/test_team.py 2012-01-27 17:43:25 +0000
317@@ -30,6 +30,7 @@
318 from lp.services.webapp.publisher import canonical_url
319 from lp.soyuz.enums import ArchiveStatus
320 from lp.testing import (
321+ login_celebrity,
322 login_person,
323 person_logged_in,
324 TestCaseWithFactory,
325@@ -173,9 +174,9 @@
326 self.acceptTeam(self.super_team, successful, failed)
327
328
329-class TestTeamEditView(TestCaseWithFactory):
330+class TestTeamPersonRenameFormMixin:
331
332- layer = LaunchpadFunctionalLayer
333+ view_name = None
334
335 def test_cannot_rename_team_with_active_ppa(self):
336 # A team with an active PPA that contains publications cannot be
337@@ -186,7 +187,7 @@
338 self.factory.makeSourcePackagePublishingHistory(archive=archive)
339 get_property_cache(team).archive = archive
340 with person_logged_in(owner):
341- view = create_initialized_view(team, name="+edit")
342+ view = create_initialized_view(team, name=self.view_name)
343 self.assertTrue(view.form_fields['name'].for_display)
344 self.assertEqual(
345 'This team has an active PPA with packages published and '
346@@ -201,7 +202,7 @@
347 removeSecurityProxy(archive).status = ArchiveStatus.DELETED
348 get_property_cache(team).archive = archive
349 with person_logged_in(owner):
350- view = create_initialized_view(team, name="+edit")
351+ view = create_initialized_view(team, name=self.view_name)
352 self.assertFalse(view.form_fields['name'].for_display)
353
354 def test_cannot_rename_team_with_active_mailinglist(self):
355@@ -211,7 +212,7 @@
356 team = self.factory.makeTeam(owner=owner)
357 self.factory.makeMailingList(team, owner)
358 with person_logged_in(owner):
359- view = create_initialized_view(team, name="+edit")
360+ view = create_initialized_view(team, name=self.view_name)
361 self.assertTrue(view.form_fields['name'].for_display)
362 self.assertEqual(
363 'This team has a mailing list and may not be renamed.',
364@@ -226,7 +227,7 @@
365 team_list.transitionToStatus(MailingListStatus.INACTIVE)
366 team_list.purge()
367 with person_logged_in(owner):
368- view = create_initialized_view(team, name="+edit")
369+ view = create_initialized_view(team, name=self.view_name)
370 self.assertFalse(view.form_fields['name'].for_display)
371
372 def test_cannot_rename_team_with_multiple_reasons(self):
373@@ -240,13 +241,19 @@
374 self.factory.makeSourcePackagePublishingHistory(archive=archive)
375 get_property_cache(team).archive = archive
376 with person_logged_in(owner):
377- view = create_initialized_view(team, name="+edit")
378+ view = create_initialized_view(team, name=self.view_name)
379 self.assertTrue(view.form_fields['name'].for_display)
380 self.assertEqual(
381 'This team has an active PPA with packages published and '
382 'a mailing list and may not be renamed.',
383 view.widgets['name'].hint)
384
385+
386+class TestTeamEditView(TestTeamPersonRenameFormMixin, TestCaseWithFactory):
387+
388+ layer = LaunchpadFunctionalLayer
389+ view_name = '+edit'
390+
391 def test_edit_team_view_permission(self):
392 # Only an administrator or the team owner of a team can
393 # change the details of that team.
394@@ -437,6 +444,29 @@
395 view.errors[0].doc())
396
397
398+class TeamAdminisiterViewTestCase(TestTeamPersonRenameFormMixin,
399+ TestCaseWithFactory):
400+
401+ layer = LaunchpadFunctionalLayer
402+ view_name = '+review'
403+
404+ def test_init_admin(self):
405+ # An admin sees all the fields.
406+ team = self.factory.makeTeam()
407+ login_celebrity('admin')
408+ view = create_initialized_view(team, name=self.view_name)
409+ self.assertEqual('Review team', view.label)
410+ self.assertEqual(
411+ ['name', 'displayname'], view.field_names)
412+
413+ def test_init_registry_expert(self):
414+ # Registry experts do not see the the displayname field.
415+ team = self.factory.makeTeam()
416+ login_celebrity('registry_experts')
417+ view = create_initialized_view(team, name=self.view_name)
418+ self.assertEqual(['name'], view.field_names)
419+
420+
421 class TestTeamMenu(TestCaseWithFactory):
422
423 layer = DatabaseFunctionalLayer