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

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 11049
Proposed branch: lp:~bac/launchpad/bug-162754
Merge into: lp:launchpad
Diff against target: 602 lines (+267/-61)
9 files modified
lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt (+9/-1)
lib/canonical/launchpad/templates/README (+0/-29)
lib/canonical/launchpad/templates/launchpad-form.pt (+8/-1)
lib/canonical/launchpad/webapp/tests/test_launchpadform.py (+5/-3)
lib/lp/app/browser/tests/launchpadform-view.txt (+44/-0)
lib/lp/registry/browser/product.py (+102/-19)
lib/lp/registry/browser/tests/product-edit-people-view.txt (+51/-4)
lib/lp/registry/stories/product/xx-product-add.txt (+48/-0)
lib/lp/registry/stories/product/xx-product-driver.txt (+0/-4)
To merge this branch: bzr merge lp:~bac/launchpad/bug-162754
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Matthew Revell (community) text Approve
Māris Fogels (community) code Approve
Review via email: mp+28227@code.launchpad.net

Commit message

Allow registrants to disclaim maintainer role of new projects and easily set the maintainer to Registry Administrators for existing projects.

Description of the change

= Summary =

Often Launchpad users will create a new project that corresponds to an
upstream in order to file a bug or perform some other action. They may
be only marginally interested in the project but are performing a
valuable service. Unfortunately since the person registered the project
they are assumed to be the maintainer and are forced into that role even
though they don't want it.

== Proposed fix ==

Provide a checkbox on project registration that allows the project to be
created but automatically re-assigned to the ~registry team.

Also on the +edit-people page a checkbox is provided to transfer the
maintainer role to ~registry.

== Pre-implementation notes ==

Chats with Curtis.

== Implementation details ==

As above.

In order to get the layout correct on the +edit-people page some
extensions needed to be added to the launchpad-form.pt. It now looks
for a widget attribute called 'widget_class' to use as the css class for
the widget.

== Tests ==

bin/test -vvt xx-product-add.txt -t xx-product-driver.txt \
-t product-edit-people-view.txt

== Demo and Q/A ==

Create a new project and look for the new checkbox on the second page.
Go to https://launchpad.dev/firefox and click on the edit icon next to
the maintainer. Look for the new checkbox.

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/stories/product/xx-product-driver.txt
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/tests/product-edit-people-view.txt
  lib/canonical/launchpad/templates/launchpad-form.pt
  lib/lp/app/browser/tests/launchpadform-view.txt
  lib/canonical/launchpad/webapp/tests/test_launchpadform.py
  lib/lp/registry/stories/product/xx-product-add.txt

== Pylint notices ==

lib/canonical/launchpad/webapp/tests/test_launchpadform.py
    57: [C0301] Line too long (79/78)

I'll fix this lint problem.

To post a comment you must log in.
Revision history for this message
Māris Fogels (mars) wrote :

Hi Brad,

The code in this branch looks good. I have one question about the tests, and a few suggestions for the UI text.

For the lanchpadform-view.txt you test for presence of extra elements when the widget_class field is present. This is the positive case. Do you need to test for the absence of those same elements when the widget_field attribute is missing?

Regarding the UI text, I found that some of the long descriptions for the new options made it unclear if the flag means "I am not the maintainer of this project" or "I do not want this project to be maintained". Specifically I would reword "but you don't want to actually maintain" to be "but you do not want to be the maintainer of". This confusion appears in two of the long descriptions.

The concept and role of the "Registry Administrators" is new to the user. I think that the short and long description during project creation does a good job of introducing this concept. However, I feel that the "Assign to Registry Administrators" control does not. Both controls result in the same outcome (the admins take over), but from a user's perspective the controls are completely different, mostly because both control's primary and secondary text share no similarity. To remedy this I suggest making all of the text for the two controls the same (or very very similar): "I do not want to maintain this project".

The code looks good, so I'm marking this as "Approved", but that is conditional upon a UI review from a certified reviewer, and a look at the text by mrevell.

Maris

review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the review Maris.

To your first point, there are two items in the form. Only one has the extra attribute and it is the only one that displays the additional CSS class. Too subtle?

Thanks for your feedback on the wording issues.

Revision history for this message
Brad Crittenden (bac) wrote :

I had one test failure running the registry suite. The fix is at http://pastebin.ubuntu.com/453606/

Revision history for this message
Matthew Revell (matthew.revell) wrote :

Thanks for this work Brad.

I agree with Mars' text suggestion. I'd refocus the descriptive text on the "I don't want to maintain this" aspect, with an explanation that registry admins will take over being secondary.

So, in the case of the first chunk of text, I'd go for something like:

"Select this if you no longer want to maintain this project in Launchpad. Launchpad's Registry Administrators team will become the project's new administrators."

Approve but I suggest changing the focus of the wording.

review: Approve (text)
Revision history for this message
Curtis Hovey (sinzui) wrote :

I really appreciate these improvements. I agree with text issue. I think both checkboxes should encapsulate the users thoughts and intent:

    [X] I don't want to maintain this

I pondered something like:

    [ user ] (Choose) or Register a team
        or [X] I don't want to maintain this

But I can find no precedence for this. I will let you and Matthew judge if a leading "or" makes this operation clearer.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt'
2--- lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt 2010-03-29 15:46:10 +0000
3+++ lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt 2010-06-24 02:23:26 +0000
4@@ -22,11 +22,13 @@
5 <...
6 <tr>
7 <td colspan="2">
8+ <div>
9 <label for="field.name">Name:</label>
10 <div>
11 <input ... name="field.name" ... />
12 </div>
13 <p class="formHelp">....</p>
14+ </div>
15 </td>
16 </tr>
17 ...
18@@ -37,12 +39,14 @@
19 <...
20 <tr>
21 <td colspan="2">
22+ <div>
23 <label for="field.contactemail">Contact Email Address:</label>
24 <span class="fieldRequired">(Optional)</span>
25 <div>
26 <input ... id="field.contactemail" ... />
27 </div>
28 <p class="formHelp">...</p>
29+ </div>
30 </td>
31 </tr>
32 ...
33@@ -57,12 +61,14 @@
34 <...
35 <tr>
36 <td colspan="2" style="text-align: left">
37+ <div>
38 <label for="field.teamdescription">Team Description:</label>
39 <span ...
40 <div><textarea ... name="field.teamdescription" ...></textarea></div>
41 <p class="formHelp">Details about the team's work, highlights, goals,
42 and how to contribute. Use plain text, paragraphs are preserved and
43 URLs are linked in pages.</p>
44+ </div>
45 </td>
46 </tr>
47 ...
48@@ -74,12 +80,14 @@
49 <...
50 <tr>
51 <td colspan="2" style="text-align: left">
52+ <div>
53 <label for="field.teamdescription">Team Description:</label>
54 <span class="fieldRequired">(Optional)</span>
55 <div><textarea ... name="field.teamdescription" ...></textarea></div>
56 <p class="formHelp">Details about the team's work, highlights, goals,
57 and how to contribute. Use plain text, paragraphs are preserved and
58 URLs are linked in pages.</p>
59+ </div>
60 </td>
61 </tr>
62 ...
63@@ -101,7 +109,7 @@
64 <td colspan="2">
65 ...
66 <input ... name="field.official_rosetta" type="checkbox" ... />
67- <label for="field.official_rosetta">Translations...</label>
68+ <label for="field.official_rosetta">Translations...</label>...
69 </td>
70 </tr>
71 ...
72
73=== removed file 'lib/canonical/launchpad/templates/README'
74--- lib/canonical/launchpad/templates/README 2005-10-31 18:29:12 +0000
75+++ lib/canonical/launchpad/templates/README 1970-01-01 00:00:00 +0000
76@@ -1,29 +0,0 @@
77-
78-STANDARD PAGES
79-
80-Please use the following as templates for your standard pages:
81-
82- - page-template.pt
83- - portlet-template.pt
84-
85-AUTOMATIC ADD/EDIT FORM MACHINERY
86-
87-In order that we get a consistent look and feel of the automatically
88-generated forms in Launchpad, please use the following process for
89-any new add/edit forms where you are using the automatic machinery:
90-
91- 1. cp launchpad-addform.pt table-add.pt
92- 2. tla add table-add.py
93- 3. vi zcml/table.pt and add the relevant <browser:addform or
94- <browser:editform section.
95-
96- - make sure you set a label="The Form Title"
97- - template="../templates/table-add.pt"
98-
99- 4. customise table-add.pt or table-edit.pt with appropriate text.
100-
101-ROCK ON!
102-
103-Please update this file as appropriate, let Mark, Steve and Stub know
104-of any changes you have made to the process.
105-
106
107=== modified file 'lib/canonical/launchpad/templates/launchpad-form.pt'
108--- lib/canonical/launchpad/templates/launchpad-form.pt 2010-06-15 03:08:22 +0000
109+++ lib/canonical/launchpad/templates/launchpad-form.pt 2010-06-24 02:23:26 +0000
110@@ -102,13 +102,15 @@
111 tal:define="field_name widget/context/__name__;
112 error python:view.getFieldError(field_name);
113 error_class python:error and 'error' or None;
114- show_optional python:view.showOptionalMarker(field_name)">
115+ show_optional python:view.showOptionalMarker(field_name);
116+ widget_class widget/widget_class|nothing">
117 <tal:is-visible condition="widget/visible">
118 <tr
119 tal:condition="python: view.isSingleLineLayout(field_name)"
120 tal:attributes="class error_class"
121 >
122 <td colspan="2">
123+ <div tal:attributes="class widget_class">
124 <tal:block tal:condition="display_label|widget/display_label|python:True">
125 <label tal:attributes="for widget/name"
126 tal:content="string:${widget/label}:">Label</label>
127@@ -124,11 +126,13 @@
128 tal:condition="widget/hint"
129 tal:content="widget/hint">Some Help Text
130 </p>
131+ </div>
132 </td>
133 </tr>
134 <tal:block condition="python: view.isMultiLineLayout(field_name)">
135 <tr tal:attributes="class error_class">
136 <td colspan="2" style="text-align: left">
137+ <div tal:attributes="class widget_class">
138 <tal:showlabel
139 condition="display_label|widget/display_label|python:True"
140 >
141@@ -151,11 +155,13 @@
142 tal:condition="widget/hint"
143 tal:content="widget/hint">Some Help Text
144 </p>
145+ </div>
146 </td>
147 </tr>
148 </tal:block>
149 <tr tal:condition="python: view.isCheckBoxLayout(field_name)">
150 <td tal:attributes="class error_class" colspan="2">
151+ <div tal:attributes="class widget_class">
152 <input type="checkbox" tal:replace="structure widget" />
153 <label tal:attributes="for widget/name"
154 tal:content="widget/label">Label</label>
155@@ -168,6 +174,7 @@
156 tal:condition="widget/hint"
157 tal:content="widget/hint">Some Help Text
158 </p>
159+ </div>
160 </td>
161 </tr>
162 </tal:is-visible>
163
164=== modified file 'lib/canonical/launchpad/webapp/tests/test_launchpadform.py'
165--- lib/canonical/launchpad/webapp/tests/test_launchpadform.py 2009-06-25 05:30:52 +0000
166+++ lib/canonical/launchpad/webapp/tests/test_launchpadform.py 2010-06-24 02:23:26 +0000
167@@ -1,7 +1,8 @@
168-# Copyright 2009 Canonical Ltd. This software is licensed under the
169+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
170 # GNU Affero General Public License version 3 (see the file LICENSE).
171
172-import unittest, doctest
173+import unittest
174+import doctest
175
176 from zope.app.form.interfaces import IDisplayWidget, IInputWidget
177 from zope.interface import directlyProvides, implements
178@@ -53,7 +54,7 @@
179 % (provides, count))
180
181 def test_showOptionalMarker(self):
182- """Verify that a field marked .for_display has no (Optional) marker."""
183+ """Verify a field marked .for_display has no (Optional) marker."""
184 # IInputWidgets have an (Optional) marker if they are not required.
185 form = LaunchpadFormView(None, None)
186 class FakeInputWidget:
187@@ -121,6 +122,7 @@
188 True
189 """
190
191+
192 def test_suite():
193 return unittest.TestSuite((
194 unittest.TestLoader().loadTestsFromName(__name__),
195
196=== added file 'lib/lp/app/browser/tests/launchpadform-view.txt'
197--- lib/lp/app/browser/tests/launchpadform-view.txt 1970-01-01 00:00:00 +0000
198+++ lib/lp/app/browser/tests/launchpadform-view.txt 2010-06-24 02:23:26 +0000
199@@ -0,0 +1,44 @@
200+Launchpadform views
201+===================
202+
203+The custom_widget accepts arbitrary attribute assignments for the
204+widget. One that launchpadform utilizes is 'widget_class'. The
205+widget rendering is wrapped with a <div> using the widget_class, which
206+can be used for subordinate field indentation, for example.
207+
208+ >>> from z3c.ptcompat import ViewPageTemplateFile
209+ >>> from zope.app.form.browser import TextWidget
210+ >>> from zope.interface import Interface
211+ >>> from zope.schema import TextLine
212+ >>> from canonical.config import config
213+ >>> from canonical.launchpad.webapp.launchpadform import (
214+ ... custom_widget, LaunchpadFormView)
215+ >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
216+ >>> from canonical.launchpad.testing.pages import find_tags_by_class
217+
218+ >>> class ITestSchema(Interface):
219+ ... displayname = TextLine(title=u"Title")
220+ ... nickname = TextLine(title=u"Nickname")
221+
222+ >>> class TestView(LaunchpadFormView):
223+ ... page_title = 'Test'
224+ ... template = ViewPageTemplateFile(
225+ ... config.root + '/lib/lp/app/templates/generic-edit.pt')
226+ ... schema = ITestSchema
227+ ... custom_widget('nickname', TextWidget,
228+ ... widget_class="field subordinate")
229+
230+ >>> login('foo.bar@canonical.com')
231+ >>> person = factory.makePerson()
232+ >>> request = LaunchpadTestRequest()
233+ >>> request.setPrincipal(person)
234+ >>> view = TestView(person, request)
235+ >>> view.initialize()
236+ >>> for tag in find_tags_by_class(view.render(), 'subordinate'):
237+ ... print tag
238+ <div class="field subordinate">
239+ <label for="field.nickname">Nickname:</label>
240+ <div>
241+ <input class="textType" id="field.nickname" name="field.nickname" ... />
242+ </div>
243+ </div>
244
245=== modified file 'lib/lp/registry/browser/product.py'
246--- lib/lp/registry/browser/product.py 2010-06-15 19:37:37 +0000
247+++ lib/lp/registry/browser/product.py 2010-06-24 02:23:26 +0000
248@@ -51,11 +51,11 @@
249
250 from zope.component import getUtility
251 from zope.event import notify
252-from zope.app.form.browser import TextAreaWidget, TextWidget
253+from zope.app.form.browser import CheckBoxWidget, TextAreaWidget, TextWidget
254 from zope.lifecycleevent import ObjectCreatedEvent
255 from zope.interface import implements, Interface
256 from zope.formlib import form
257-from zope.schema import Choice
258+from zope.schema import Bool, Choice
259 from zope.schema.vocabulary import (
260 SimpleVocabulary, SimpleTerm)
261 from zope.security.proxy import removeSecurityProxy
262@@ -66,6 +66,7 @@
263
264 from canonical.config import config
265 from lazr.delegates import delegates
266+from lazr.restful.interface import copy_field
267 from canonical.launchpad import _
268 from canonical.launchpad.fields import PillarAliases, PublicPersonChoice
269 from lp.app.interfaces.headings import IEditableContextTitle
270@@ -1823,7 +1824,8 @@
271 """Step 2 (of 2) in the +new project add wizard."""
272
273 _field_names = ['displayname', 'name', 'title', 'summary',
274- 'description', 'licenses', 'license_info']
275+ 'description', 'licenses', 'license_info',
276+ ]
277 main_action_label = u'Complete Registration'
278 schema = IProduct
279 step_name = 'projectaddstep2'
280@@ -1844,6 +1846,33 @@
281 return 'Check for duplicate projects'
282 return 'Registration details'
283
284+ def setUpFields(self):
285+ """See `LaunchpadFormView`."""
286+ super(ProjectAddStepTwo, self).setUpFields()
287+ self.form_fields = (self.form_fields +
288+ self._createDisclaimMaintainerField())
289+
290+ def _createDisclaimMaintainerField(self):
291+ """Return a Bool field for disclaiming maintainer.
292+
293+ If the registrant does not want to maintain the project she can select
294+ this checkbox and the ownership will be transfered to the registry
295+ admins team.
296+ """
297+
298+ return form.Fields(
299+ Bool(__name__='disclaim_maintainer',
300+ title=_("I do not want to maintain this project"),
301+ description=_(
302+ "Select if you are registering this project "
303+ "for the purpose of taking an action (such as "
304+ "reporting a bug) but you don't want to actually "
305+ "maintain the project in Launchpad. "
306+ "The Registry Administrators team will become "
307+ "the maintainers until a community maintainer "
308+ "can be found.")),
309+ render_context=self.render_context)
310+
311 def setUpWidgets(self):
312 """See `LaunchpadFormView`."""
313 super(ProjectAddStepTwo, self).setUpWidgets()
314@@ -1903,8 +1932,14 @@
315 # Get optional data.
316 project = data.get('project')
317 description = data.get('description')
318+ disclaim_maintainer = data.get('disclaim_maintainer', False)
319+ if disclaim_maintainer:
320+ owner = getUtility(ILaunchpadCelebrities).registry_experts
321+ else:
322+ owner = self.user
323 return getUtility(IProductSet).createProduct(
324- owner=self.user,
325+ registrant=self.user,
326+ owner=owner,
327 name=data['name'],
328 displayname=data['displayname'],
329 title=data['title'],
330@@ -1934,20 +1969,50 @@
331 return ProjectAddStepOne
332
333
334+class IProductEditPeopleSchema(Interface):
335+ """Defines the fields for the edit form.
336+
337+ Specifically adds a new checkbox for transferring the maintainer role to
338+ Registry Administrators and makes the owner optional.
339+ """
340+ owner = copy_field(IProduct['owner'])
341+ owner.required = False
342+
343+ driver = copy_field(IProduct['driver'])
344+
345+ transfer_to_registry = Bool(
346+ title=_("I do not want to maintain this project"),
347+ required=False,
348+ description=_(
349+ "Select this if you no longer want to maintain this project in "
350+ "Launchpad. Launchpad's Registry Administrators team will "
351+ "become the project's new maintainers."))
352+
353+
354 class ProductEditPeopleView(LaunchpadEditFormView):
355 """Enable editing of important people on the project."""
356
357 implements(IProductEditMenu)
358
359 label = "Change the roles of people"
360- schema = IProduct
361+ schema = IProductEditPeopleSchema
362 field_names = [
363 'owner',
364+ 'transfer_to_registry',
365 'driver',
366 ]
367
368+ for_input = True
369+
370+ # Initial value must be provided for the 'transfer_to_registry' field to
371+ # avoid having the non-existent attribute queried on the context and
372+ # failing.
373+ initial_values = {'transfer_to_registry': False}
374+
375 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",
376 include_create_team_link=True)
377+ custom_widget('transfer_to_registry', CheckBoxWidget,
378+ widget_class='field subordinate')
379 custom_widget('driver', PersonPickerWidget, header="Select the driver",
380 include_create_team_link=True)
381
382@@ -1956,24 +2021,37 @@
383 """The HTML page title."""
384 return "Change the roles of %s's people" % self.context.title
385
386+ def validate(self, data):
387+ """Validate owner and transfer_to_registry are consistent.
388+
389+ At most one may be specified.
390+ """
391+ # If errors have already been found we can skip validation.
392+ if len(self.errors) > 0:
393+ return
394+ xfer = data.get('transfer_to_registry', False)
395+ owner = data.get('owner')
396+ if owner is not None and xfer:
397+ self.setFieldError(
398+ 'owner',
399+ 'You may not specify a new owner if you '
400+ 'select the checkbox.')
401+ elif xfer:
402+ data['owner'] = getUtility(ILaunchpadCelebrities).registry_experts
403+ elif owner is None:
404+ self.setFieldError(
405+ 'owner',
406+ 'You must specify a maintainer or select '
407+ 'the checkbox.')
408+
409 @action(_('Save changes'), name='save')
410 def save_action(self, action, data):
411 """Save the changes to the associated people."""
412- old_owner = self.context.owner
413- old_driver = self.context.driver
414+ # Since 'transfer_to_registry' is not a real attribute on a Product,
415+ # it must be removed from data before the context is updated.
416+ if 'transfer_to_registry' in data:
417+ del data['transfer_to_registry']
418 self.updateContextFromData(data)
419- if self.context.owner != old_owner:
420- self.request.response.addNotification(
421- "Successfully changed the maintainer to %s"
422- % self.context.owner.displayname)
423- if self.context.driver != old_driver:
424- if self.context.driver is not None:
425- self.request.response.addNotification(
426- "Successfully changed the driver to %s"
427- % self.context.driver.displayname)
428- else:
429- self.request.response.addNotification(
430- "Successfully removed the driver")
431
432 @property
433 def next_url(self):
434@@ -1984,3 +2062,8 @@
435 def cancel_url(self):
436 """See `LaunchpadFormView`."""
437 return canonical_url(self.context)
438+
439+ @property
440+ def adapters(self):
441+ """See `LaunchpadFormView`"""
442+ return {IProductEditPeopleSchema: self.context}
443
444=== modified file 'lib/lp/registry/browser/tests/product-edit-people-view.txt'
445--- lib/lp/registry/browser/tests/product-edit-people-view.txt 2009-10-26 18:40:04 +0000
446+++ lib/lp/registry/browser/tests/product-edit-people-view.txt 2010-06-24 02:23:26 +0000
447@@ -1,10 +1,13 @@
448 ProductEditPeopleView
449 =====================
450
451+Artifact reassignment
452+---------------------
453+
454 When a product is re-assigned to another person, objects related to that
455 product (product series, product releases and translations in the import
456-queue) owned by the same registrant are also re-assigned to the new
457-registrant.
458+queue) owned by the same owner/maintainer are also re-assigned to the new
459+owner/maintainer.
460
461 Firefox is owned by Sample Person (name12)
462
463@@ -43,7 +46,7 @@
464 name12
465
466 No Privileges Person is taking over the project, but he cannot access the
467-view because he is not yet an owner or admin.
468+view because he is not yet an owner/maintainer or admin.
469
470 >>> from canonical.launchpad.webapp.authorization import check_permission
471
472@@ -52,7 +55,8 @@
473 >>> check_permission('launchpad.Edit', view)
474 False
475
476-Sample person, as the owner can change the registrant to No Privileges Person.
477+Sample person, as the owner/maintainer can change the owner/maintainer
478+to No Privileges Person.
479
480 >>> login_person(sample_person)
481 >>> form = {
482@@ -85,3 +89,46 @@
483 >>> print entry[0].importer.name
484 no-priv
485
486+Assigning to Registry Administrators
487+------------------------------------
488+
489+As a short-cut, a checkbox is presented to disclaim the maintainer
490+role and transfer it to the Registry Administrators team.
491+
492+ >>> login_person(sample_person)
493+ >>> product = factory.makeProduct(owner=sample_person)
494+
495+ >>> form = {
496+ ... 'field.transfer_to_registry': 'on',
497+ ... 'field.actions.save': 'Save changes',
498+ ... }
499+
500+ >>> view = create_initialized_view(product, '+edit-people', form=form)
501+ >>> view.errors
502+ []
503+
504+ >>> product.owner.name
505+ u'registry'
506+
507+Not specifying the owner/maintainer nor checking the checkbox is an error.
508+
509+ >>> form = {
510+ ... 'field.actions.save': 'Save changes',
511+ ... }
512+
513+ >>> view = create_initialized_view(product, '+edit-people', form=form)
514+ >>> view.errors
515+ [u'You must specify a maintainer or select the checkbox.']
516+
517+Selecting both the owner/maintainer and the checkbox is also an error.
518+
519+ >>> product = factory.makeProduct(owner=sample_person)
520+ >>> form = {
521+ ... 'field.owner': 'no-priv',
522+ ... 'field.transfer_to_registry': 'on',
523+ ... 'field.actions.save': 'Save changes',
524+ ... }
525+
526+ >>> view = create_initialized_view(product, '+edit-people', form=form)
527+ >>> view.errors
528+ [u'You may not specify a new owner if you select the checkbox.']
529
530=== modified file 'lib/lp/registry/stories/product/xx-product-add.txt'
531--- lib/lp/registry/stories/product/xx-product-add.txt 2010-06-12 05:14:55 +0000
532+++ lib/lp/registry/stories/product/xx-product-add.txt 2010-06-24 02:23:26 +0000
533@@ -136,6 +136,54 @@
534 >>> print extract_text(desc)
535 The desktop aardvark is an ornery thing.
536
537+Let's ensure the registrant and maintainer are listed correctly.
538+
539+ >>> registrant = find_tag_by_id(user_browser.contents,
540+ ... 'registration')
541+ >>> print extract_text(registrant)
542+ Registered...by...No Privileges Person...
543+
544+ >>> maintainer = find_tag_by_id(user_browser.contents,
545+ ... 'owner')
546+ >>> print extract_text(maintainer)
547+ Maintainer: No Privileges Person...
548+
549+
550+Turning over maintainership
551+---------------------------
552+
553+Sample Person wants to create a project in Launchpad for a project
554+that exists elsewhere as an upstream. She wants it to exist in
555+Launchpad so she can file a bug, for instance, but she is not
556+interested in being the project maintainer for the long run.
557+
558+ >>> user_browser.open('http://launchpad.dev')
559+ >>> user_browser.getLink('Register a project').click()
560+
561+ >>> user_browser.getControl('Name').value = 'kittyhawk'
562+ >>> user_browser.getControl('URL').value = 'kittyhawk'
563+ >>> user_browser.getControl('Title').value = 'Kitty Hawk ATC'
564+ >>> user_browser.getControl('Summary').value = (
565+ ... 'Kitty Hawk Air Traffic Simulator')
566+ >>> user_browser.getControl('Continue').click()
567+ >>> user_browser.getControl('Python License').click()
568+ >>> disclaim = user_browser.getControl(name='field.disclaim_maintainer')
569+ >>> disclaim.value = ['checked']
570+ >>> user_browser.getControl('Complete Registration').click()
571+
572+Sample person is shown as the registrant but the maintainer is now
573+Registry Admins.
574+
575+ >>> registrant = find_tag_by_id(user_browser.contents,
576+ ... 'registration')
577+ >>> print extract_text(registrant)
578+ Registered...by...No Privileges Person...
579+
580+ >>> maintainer = find_tag_by_id(user_browser.contents,
581+ ... 'owner')
582+ >>> print extract_text(maintainer)
583+ Maintainer: Registry Administrators...
584+
585
586 Search results
587 --------------
588
589=== modified file 'lib/lp/registry/stories/product/xx-product-driver.txt'
590--- lib/lp/registry/stories/product/xx-product-driver.txt 2009-10-08 16:29:45 +0000
591+++ lib/lp/registry/stories/product/xx-product-driver.txt 2010-06-24 02:23:26 +0000
592@@ -22,10 +22,6 @@
593 >>> print browser.url
594 http://launchpad.dev/firefox
595
596- >>> for tag in find_tags_by_class(browser.contents, 'informational'):
597- ... print tag.renderContents()
598- Successfully changed the driver to Sample Person
599-
600 Sample Person is listed as the driver of the product.
601
602 >>> main = find_main_content(browser.contents)