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
=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt'
--- lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt 2010-03-29 15:46:10 +0000
+++ lib/canonical/launchpad/pagetests/standalone/xx-form-layout.txt 2010-06-24 02:23:26 +0000
@@ -22,11 +22,13 @@
22 <...22 <...
23 <tr>23 <tr>
24 <td colspan="2">24 <td colspan="2">
25 <div>
25 <label for="field.name">Name:</label>26 <label for="field.name">Name:</label>
26 <div>27 <div>
27 <input ... name="field.name" ... />28 <input ... name="field.name" ... />
28 </div>29 </div>
29 <p class="formHelp">....</p>30 <p class="formHelp">....</p>
31 </div>
30 </td>32 </td>
31 </tr>33 </tr>
32 ...34 ...
@@ -37,12 +39,14 @@
37 <...39 <...
38 <tr>40 <tr>
39 <td colspan="2">41 <td colspan="2">
42 <div>
40 <label for="field.contactemail">Contact Email Address:</label>43 <label for="field.contactemail">Contact Email Address:</label>
41 <span class="fieldRequired">(Optional)</span>44 <span class="fieldRequired">(Optional)</span>
42 <div>45 <div>
43 <input ... id="field.contactemail" ... />46 <input ... id="field.contactemail" ... />
44 </div>47 </div>
45 <p class="formHelp">...</p>48 <p class="formHelp">...</p>
49 </div>
46 </td>50 </td>
47 </tr>51 </tr>
48 ...52 ...
@@ -57,12 +61,14 @@
57 <...61 <...
58 <tr>62 <tr>
59 <td colspan="2" style="text-align: left">63 <td colspan="2" style="text-align: left">
64 <div>
60 <label for="field.teamdescription">Team Description:</label>65 <label for="field.teamdescription">Team Description:</label>
61 <span ...66 <span ...
62 <div><textarea ... name="field.teamdescription" ...></textarea></div>67 <div><textarea ... name="field.teamdescription" ...></textarea></div>
63 <p class="formHelp">Details about the team's work, highlights, goals,68 <p class="formHelp">Details about the team's work, highlights, goals,
64 and how to contribute. Use plain text, paragraphs are preserved and69 and how to contribute. Use plain text, paragraphs are preserved and
65 URLs are linked in pages.</p>70 URLs are linked in pages.</p>
71 </div>
66 </td>72 </td>
67 </tr>73 </tr>
68 ...74 ...
@@ -74,12 +80,14 @@
74 <...80 <...
75 <tr>81 <tr>
76 <td colspan="2" style="text-align: left">82 <td colspan="2" style="text-align: left">
83 <div>
77 <label for="field.teamdescription">Team Description:</label>84 <label for="field.teamdescription">Team Description:</label>
78 <span class="fieldRequired">(Optional)</span>85 <span class="fieldRequired">(Optional)</span>
79 <div><textarea ... name="field.teamdescription" ...></textarea></div>86 <div><textarea ... name="field.teamdescription" ...></textarea></div>
80 <p class="formHelp">Details about the team's work, highlights, goals,87 <p class="formHelp">Details about the team's work, highlights, goals,
81 and how to contribute. Use plain text, paragraphs are preserved and88 and how to contribute. Use plain text, paragraphs are preserved and
82 URLs are linked in pages.</p>89 URLs are linked in pages.</p>
90 </div>
83 </td>91 </td>
84 </tr>92 </tr>
85 ...93 ...
@@ -101,7 +109,7 @@
101 <td colspan="2">109 <td colspan="2">
102 ...110 ...
103 <input ... name="field.official_rosetta" type="checkbox" ... />111 <input ... name="field.official_rosetta" type="checkbox" ... />
104 <label for="field.official_rosetta">Translations...</label>112 <label for="field.official_rosetta">Translations...</label>...
105 </td>113 </td>
106 </tr>114 </tr>
107 ...115 ...
108116
=== removed file 'lib/canonical/launchpad/templates/README'
--- lib/canonical/launchpad/templates/README 2005-10-31 18:29:12 +0000
+++ lib/canonical/launchpad/templates/README 1970-01-01 00:00:00 +0000
@@ -1,29 +0,0 @@
1
2STANDARD PAGES
3
4Please use the following as templates for your standard pages:
5
6 - page-template.pt
7 - portlet-template.pt
8
9AUTOMATIC ADD/EDIT FORM MACHINERY
10
11In order that we get a consistent look and feel of the automatically
12generated forms in Launchpad, please use the following process for
13any new add/edit forms where you are using the automatic machinery:
14
15 1. cp launchpad-addform.pt table-add.pt
16 2. tla add table-add.py
17 3. vi zcml/table.pt and add the relevant <browser:addform or
18 <browser:editform section.
19
20 - make sure you set a label="The Form Title"
21 - template="../templates/table-add.pt"
22
23 4. customise table-add.pt or table-edit.pt with appropriate text.
24
25ROCK ON!
26
27Please update this file as appropriate, let Mark, Steve and Stub know
28of any changes you have made to the process.
29
300
=== modified file 'lib/canonical/launchpad/templates/launchpad-form.pt'
--- lib/canonical/launchpad/templates/launchpad-form.pt 2010-06-15 03:08:22 +0000
+++ lib/canonical/launchpad/templates/launchpad-form.pt 2010-06-24 02:23:26 +0000
@@ -102,13 +102,15 @@
102 tal:define="field_name widget/context/__name__;102 tal:define="field_name widget/context/__name__;
103 error python:view.getFieldError(field_name);103 error python:view.getFieldError(field_name);
104 error_class python:error and 'error' or None;104 error_class python:error and 'error' or None;
105 show_optional python:view.showOptionalMarker(field_name)">105 show_optional python:view.showOptionalMarker(field_name);
106 widget_class widget/widget_class|nothing">
106 <tal:is-visible condition="widget/visible">107 <tal:is-visible condition="widget/visible">
107 <tr108 <tr
108 tal:condition="python: view.isSingleLineLayout(field_name)"109 tal:condition="python: view.isSingleLineLayout(field_name)"
109 tal:attributes="class error_class"110 tal:attributes="class error_class"
110 >111 >
111 <td colspan="2">112 <td colspan="2">
113 <div tal:attributes="class widget_class">
112 <tal:block tal:condition="display_label|widget/display_label|python:True">114 <tal:block tal:condition="display_label|widget/display_label|python:True">
113 <label tal:attributes="for widget/name"115 <label tal:attributes="for widget/name"
114 tal:content="string:${widget/label}:">Label</label>116 tal:content="string:${widget/label}:">Label</label>
@@ -124,11 +126,13 @@
124 tal:condition="widget/hint"126 tal:condition="widget/hint"
125 tal:content="widget/hint">Some Help Text127 tal:content="widget/hint">Some Help Text
126 </p>128 </p>
129 </div>
127 </td>130 </td>
128 </tr>131 </tr>
129 <tal:block condition="python: view.isMultiLineLayout(field_name)">132 <tal:block condition="python: view.isMultiLineLayout(field_name)">
130 <tr tal:attributes="class error_class">133 <tr tal:attributes="class error_class">
131 <td colspan="2" style="text-align: left">134 <td colspan="2" style="text-align: left">
135 <div tal:attributes="class widget_class">
132 <tal:showlabel136 <tal:showlabel
133 condition="display_label|widget/display_label|python:True"137 condition="display_label|widget/display_label|python:True"
134 >138 >
@@ -151,11 +155,13 @@
151 tal:condition="widget/hint"155 tal:condition="widget/hint"
152 tal:content="widget/hint">Some Help Text156 tal:content="widget/hint">Some Help Text
153 </p>157 </p>
158 </div>
154 </td>159 </td>
155 </tr>160 </tr>
156 </tal:block>161 </tal:block>
157 <tr tal:condition="python: view.isCheckBoxLayout(field_name)">162 <tr tal:condition="python: view.isCheckBoxLayout(field_name)">
158 <td tal:attributes="class error_class" colspan="2">163 <td tal:attributes="class error_class" colspan="2">
164 <div tal:attributes="class widget_class">
159 <input type="checkbox" tal:replace="structure widget" />165 <input type="checkbox" tal:replace="structure widget" />
160 <label tal:attributes="for widget/name"166 <label tal:attributes="for widget/name"
161 tal:content="widget/label">Label</label>167 tal:content="widget/label">Label</label>
@@ -168,6 +174,7 @@
168 tal:condition="widget/hint"174 tal:condition="widget/hint"
169 tal:content="widget/hint">Some Help Text175 tal:content="widget/hint">Some Help Text
170 </p>176 </p>
177 </div>
171 </td>178 </td>
172 </tr>179 </tr>
173 </tal:is-visible>180 </tal:is-visible>
174181
=== modified file 'lib/canonical/launchpad/webapp/tests/test_launchpadform.py'
--- lib/canonical/launchpad/webapp/tests/test_launchpadform.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/webapp/tests/test_launchpadform.py 2010-06-24 02:23:26 +0000
@@ -1,7 +1,8 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import unittest, doctest4import unittest
5import doctest
56
6from zope.app.form.interfaces import IDisplayWidget, IInputWidget7from zope.app.form.interfaces import IDisplayWidget, IInputWidget
7from zope.interface import directlyProvides, implements8from zope.interface import directlyProvides, implements
@@ -53,7 +54,7 @@
53 % (provides, count))54 % (provides, count))
5455
55 def test_showOptionalMarker(self):56 def test_showOptionalMarker(self):
56 """Verify that a field marked .for_display has no (Optional) marker."""57 """Verify a field marked .for_display has no (Optional) marker."""
57 # IInputWidgets have an (Optional) marker if they are not required.58 # IInputWidgets have an (Optional) marker if they are not required.
58 form = LaunchpadFormView(None, None)59 form = LaunchpadFormView(None, None)
59 class FakeInputWidget:60 class FakeInputWidget:
@@ -121,6 +122,7 @@
121 True122 True
122 """123 """
123124
125
124def test_suite():126def test_suite():
125 return unittest.TestSuite((127 return unittest.TestSuite((
126 unittest.TestLoader().loadTestsFromName(__name__),128 unittest.TestLoader().loadTestsFromName(__name__),
127129
=== added file 'lib/lp/app/browser/tests/launchpadform-view.txt'
--- lib/lp/app/browser/tests/launchpadform-view.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/app/browser/tests/launchpadform-view.txt 2010-06-24 02:23:26 +0000
@@ -0,0 +1,44 @@
1Launchpadform views
2===================
3
4The custom_widget accepts arbitrary attribute assignments for the
5widget. One that launchpadform utilizes is 'widget_class'. The
6widget rendering is wrapped with a <div> using the widget_class, which
7can be used for subordinate field indentation, for example.
8
9 >>> from z3c.ptcompat import ViewPageTemplateFile
10 >>> from zope.app.form.browser import TextWidget
11 >>> from zope.interface import Interface
12 >>> from zope.schema import TextLine
13 >>> from canonical.config import config
14 >>> from canonical.launchpad.webapp.launchpadform import (
15 ... custom_widget, LaunchpadFormView)
16 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
17 >>> from canonical.launchpad.testing.pages import find_tags_by_class
18
19 >>> class ITestSchema(Interface):
20 ... displayname = TextLine(title=u"Title")
21 ... nickname = TextLine(title=u"Nickname")
22
23 >>> class TestView(LaunchpadFormView):
24 ... page_title = 'Test'
25 ... template = ViewPageTemplateFile(
26 ... config.root + '/lib/lp/app/templates/generic-edit.pt')
27 ... schema = ITestSchema
28 ... custom_widget('nickname', TextWidget,
29 ... widget_class="field subordinate")
30
31 >>> login('foo.bar@canonical.com')
32 >>> person = factory.makePerson()
33 >>> request = LaunchpadTestRequest()
34 >>> request.setPrincipal(person)
35 >>> view = TestView(person, request)
36 >>> view.initialize()
37 >>> for tag in find_tags_by_class(view.render(), 'subordinate'):
38 ... print tag
39 <div class="field subordinate">
40 <label for="field.nickname">Nickname:</label>
41 <div>
42 <input class="textType" id="field.nickname" name="field.nickname" ... />
43 </div>
44 </div>
045
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2010-06-15 19:37:37 +0000
+++ lib/lp/registry/browser/product.py 2010-06-24 02:23:26 +0000
@@ -51,11 +51,11 @@
5151
52from zope.component import getUtility52from zope.component import getUtility
53from zope.event import notify53from zope.event import notify
54from zope.app.form.browser import TextAreaWidget, TextWidget54from zope.app.form.browser import CheckBoxWidget, TextAreaWidget, TextWidget
55from zope.lifecycleevent import ObjectCreatedEvent55from zope.lifecycleevent import ObjectCreatedEvent
56from zope.interface import implements, Interface56from zope.interface import implements, Interface
57from zope.formlib import form57from zope.formlib import form
58from zope.schema import Choice58from zope.schema import Bool, Choice
59from zope.schema.vocabulary import (59from zope.schema.vocabulary import (
60 SimpleVocabulary, SimpleTerm)60 SimpleVocabulary, SimpleTerm)
61from zope.security.proxy import removeSecurityProxy61from zope.security.proxy import removeSecurityProxy
@@ -66,6 +66,7 @@
6666
67from canonical.config import config67from canonical.config import config
68from lazr.delegates import delegates68from lazr.delegates import delegates
69from lazr.restful.interface import copy_field
69from canonical.launchpad import _70from canonical.launchpad import _
70from canonical.launchpad.fields import PillarAliases, PublicPersonChoice71from canonical.launchpad.fields import PillarAliases, PublicPersonChoice
71from lp.app.interfaces.headings import IEditableContextTitle72from lp.app.interfaces.headings import IEditableContextTitle
@@ -1823,7 +1824,8 @@
1823 """Step 2 (of 2) in the +new project add wizard."""1824 """Step 2 (of 2) in the +new project add wizard."""
18241825
1825 _field_names = ['displayname', 'name', 'title', 'summary',1826 _field_names = ['displayname', 'name', 'title', 'summary',
1826 'description', 'licenses', 'license_info']1827 'description', 'licenses', 'license_info',
1828 ]
1827 main_action_label = u'Complete Registration'1829 main_action_label = u'Complete Registration'
1828 schema = IProduct1830 schema = IProduct
1829 step_name = 'projectaddstep2'1831 step_name = 'projectaddstep2'
@@ -1844,6 +1846,33 @@
1844 return 'Check for duplicate projects'1846 return 'Check for duplicate projects'
1845 return 'Registration details'1847 return 'Registration details'
18461848
1849 def setUpFields(self):
1850 """See `LaunchpadFormView`."""
1851 super(ProjectAddStepTwo, self).setUpFields()
1852 self.form_fields = (self.form_fields +
1853 self._createDisclaimMaintainerField())
1854
1855 def _createDisclaimMaintainerField(self):
1856 """Return a Bool field for disclaiming maintainer.
1857
1858 If the registrant does not want to maintain the project she can select
1859 this checkbox and the ownership will be transfered to the registry
1860 admins team.
1861 """
1862
1863 return form.Fields(
1864 Bool(__name__='disclaim_maintainer',
1865 title=_("I do not want to maintain this project"),
1866 description=_(
1867 "Select if you are registering this project "
1868 "for the purpose of taking an action (such as "
1869 "reporting a bug) but you don't want to actually "
1870 "maintain the project in Launchpad. "
1871 "The Registry Administrators team will become "
1872 "the maintainers until a community maintainer "
1873 "can be found.")),
1874 render_context=self.render_context)
1875
1847 def setUpWidgets(self):1876 def setUpWidgets(self):
1848 """See `LaunchpadFormView`."""1877 """See `LaunchpadFormView`."""
1849 super(ProjectAddStepTwo, self).setUpWidgets()1878 super(ProjectAddStepTwo, self).setUpWidgets()
@@ -1903,8 +1932,14 @@
1903 # Get optional data.1932 # Get optional data.
1904 project = data.get('project')1933 project = data.get('project')
1905 description = data.get('description')1934 description = data.get('description')
1935 disclaim_maintainer = data.get('disclaim_maintainer', False)
1936 if disclaim_maintainer:
1937 owner = getUtility(ILaunchpadCelebrities).registry_experts
1938 else:
1939 owner = self.user
1906 return getUtility(IProductSet).createProduct(1940 return getUtility(IProductSet).createProduct(
1907 owner=self.user,1941 registrant=self.user,
1942 owner=owner,
1908 name=data['name'],1943 name=data['name'],
1909 displayname=data['displayname'],1944 displayname=data['displayname'],
1910 title=data['title'],1945 title=data['title'],
@@ -1934,20 +1969,50 @@
1934 return ProjectAddStepOne1969 return ProjectAddStepOne
19351970
19361971
1972class IProductEditPeopleSchema(Interface):
1973 """Defines the fields for the edit form.
1974
1975 Specifically adds a new checkbox for transferring the maintainer role to
1976 Registry Administrators and makes the owner optional.
1977 """
1978 owner = copy_field(IProduct['owner'])
1979 owner.required = False
1980
1981 driver = copy_field(IProduct['driver'])
1982
1983 transfer_to_registry = Bool(
1984 title=_("I do not want to maintain this project"),
1985 required=False,
1986 description=_(
1987 "Select this if you no longer want to maintain this project in "
1988 "Launchpad. Launchpad's Registry Administrators team will "
1989 "become the project's new maintainers."))
1990
1991
1937class ProductEditPeopleView(LaunchpadEditFormView):1992class ProductEditPeopleView(LaunchpadEditFormView):
1938 """Enable editing of important people on the project."""1993 """Enable editing of important people on the project."""
19391994
1940 implements(IProductEditMenu)1995 implements(IProductEditMenu)
19411996
1942 label = "Change the roles of people"1997 label = "Change the roles of people"
1943 schema = IProduct1998 schema = IProductEditPeopleSchema
1944 field_names = [1999 field_names = [
1945 'owner',2000 'owner',
2001 'transfer_to_registry',
1946 'driver',2002 'driver',
1947 ]2003 ]
19482004
2005 for_input = True
2006
2007 # Initial value must be provided for the 'transfer_to_registry' field to
2008 # avoid having the non-existent attribute queried on the context and
2009 # failing.
2010 initial_values = {'transfer_to_registry': False}
2011
1949 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",2012 custom_widget('owner', PersonPickerWidget, header="Select the maintainer",
1950 include_create_team_link=True)2013 include_create_team_link=True)
2014 custom_widget('transfer_to_registry', CheckBoxWidget,
2015 widget_class='field subordinate')
1951 custom_widget('driver', PersonPickerWidget, header="Select the driver",2016 custom_widget('driver', PersonPickerWidget, header="Select the driver",
1952 include_create_team_link=True)2017 include_create_team_link=True)
19532018
@@ -1956,24 +2021,37 @@
1956 """The HTML page title."""2021 """The HTML page title."""
1957 return "Change the roles of %s's people" % self.context.title2022 return "Change the roles of %s's people" % self.context.title
19582023
2024 def validate(self, data):
2025 """Validate owner and transfer_to_registry are consistent.
2026
2027 At most one may be specified.
2028 """
2029 # If errors have already been found we can skip validation.
2030 if len(self.errors) > 0:
2031 return
2032 xfer = data.get('transfer_to_registry', False)
2033 owner = data.get('owner')
2034 if owner is not None and xfer:
2035 self.setFieldError(
2036 'owner',
2037 'You may not specify a new owner if you '
2038 'select the checkbox.')
2039 elif xfer:
2040 data['owner'] = getUtility(ILaunchpadCelebrities).registry_experts
2041 elif owner is None:
2042 self.setFieldError(
2043 'owner',
2044 'You must specify a maintainer or select '
2045 'the checkbox.')
2046
1959 @action(_('Save changes'), name='save')2047 @action(_('Save changes'), name='save')
1960 def save_action(self, action, data):2048 def save_action(self, action, data):
1961 """Save the changes to the associated people."""2049 """Save the changes to the associated people."""
1962 old_owner = self.context.owner2050 # Since 'transfer_to_registry' is not a real attribute on a Product,
1963 old_driver = self.context.driver2051 # it must be removed from data before the context is updated.
2052 if 'transfer_to_registry' in data:
2053 del data['transfer_to_registry']
1964 self.updateContextFromData(data)2054 self.updateContextFromData(data)
1965 if self.context.owner != old_owner:
1966 self.request.response.addNotification(
1967 "Successfully changed the maintainer to %s"
1968 % self.context.owner.displayname)
1969 if self.context.driver != old_driver:
1970 if self.context.driver is not None:
1971 self.request.response.addNotification(
1972 "Successfully changed the driver to %s"
1973 % self.context.driver.displayname)
1974 else:
1975 self.request.response.addNotification(
1976 "Successfully removed the driver")
19772055
1978 @property2056 @property
1979 def next_url(self):2057 def next_url(self):
@@ -1984,3 +2062,8 @@
1984 def cancel_url(self):2062 def cancel_url(self):
1985 """See `LaunchpadFormView`."""2063 """See `LaunchpadFormView`."""
1986 return canonical_url(self.context)2064 return canonical_url(self.context)
2065
2066 @property
2067 def adapters(self):
2068 """See `LaunchpadFormView`"""
2069 return {IProductEditPeopleSchema: self.context}
19872070
=== modified file 'lib/lp/registry/browser/tests/product-edit-people-view.txt'
--- lib/lp/registry/browser/tests/product-edit-people-view.txt 2009-10-26 18:40:04 +0000
+++ lib/lp/registry/browser/tests/product-edit-people-view.txt 2010-06-24 02:23:26 +0000
@@ -1,10 +1,13 @@
1ProductEditPeopleView1ProductEditPeopleView
2=====================2=====================
33
4Artifact reassignment
5---------------------
6
4When a product is re-assigned to another person, objects related to that7When a product is re-assigned to another person, objects related to that
5product (product series, product releases and translations in the import8product (product series, product releases and translations in the import
6queue) owned by the same registrant are also re-assigned to the new9queue) owned by the same owner/maintainer are also re-assigned to the new
7registrant.10owner/maintainer.
811
9Firefox is owned by Sample Person (name12)12Firefox is owned by Sample Person (name12)
1013
@@ -43,7 +46,7 @@
43 name1246 name12
4447
45No Privileges Person is taking over the project, but he cannot access the48No Privileges Person is taking over the project, but he cannot access the
46view because he is not yet an owner or admin.49view because he is not yet an owner/maintainer or admin.
4750
48 >>> from canonical.launchpad.webapp.authorization import check_permission51 >>> from canonical.launchpad.webapp.authorization import check_permission
4952
@@ -52,7 +55,8 @@
52 >>> check_permission('launchpad.Edit', view)55 >>> check_permission('launchpad.Edit', view)
53 False56 False
5457
55Sample person, as the owner can change the registrant to No Privileges Person.58Sample person, as the owner/maintainer can change the owner/maintainer
59to No Privileges Person.
5660
57 >>> login_person(sample_person)61 >>> login_person(sample_person)
58 >>> form = {62 >>> form = {
@@ -85,3 +89,46 @@
85 >>> print entry[0].importer.name89 >>> print entry[0].importer.name
86 no-priv90 no-priv
8791
92Assigning to Registry Administrators
93------------------------------------
94
95As a short-cut, a checkbox is presented to disclaim the maintainer
96role and transfer it to the Registry Administrators team.
97
98 >>> login_person(sample_person)
99 >>> product = factory.makeProduct(owner=sample_person)
100
101 >>> form = {
102 ... 'field.transfer_to_registry': 'on',
103 ... 'field.actions.save': 'Save changes',
104 ... }
105
106 >>> view = create_initialized_view(product, '+edit-people', form=form)
107 >>> view.errors
108 []
109
110 >>> product.owner.name
111 u'registry'
112
113Not specifying the owner/maintainer nor checking the checkbox is an error.
114
115 >>> form = {
116 ... 'field.actions.save': 'Save changes',
117 ... }
118
119 >>> view = create_initialized_view(product, '+edit-people', form=form)
120 >>> view.errors
121 [u'You must specify a maintainer or select the checkbox.']
122
123Selecting both the owner/maintainer and the checkbox is also an error.
124
125 >>> product = factory.makeProduct(owner=sample_person)
126 >>> form = {
127 ... 'field.owner': 'no-priv',
128 ... 'field.transfer_to_registry': 'on',
129 ... 'field.actions.save': 'Save changes',
130 ... }
131
132 >>> view = create_initialized_view(product, '+edit-people', form=form)
133 >>> view.errors
134 [u'You may not specify a new owner if you select the checkbox.']
88135
=== modified file 'lib/lp/registry/stories/product/xx-product-add.txt'
--- lib/lp/registry/stories/product/xx-product-add.txt 2010-06-12 05:14:55 +0000
+++ lib/lp/registry/stories/product/xx-product-add.txt 2010-06-24 02:23:26 +0000
@@ -136,6 +136,54 @@
136 >>> print extract_text(desc)136 >>> print extract_text(desc)
137 The desktop aardvark is an ornery thing.137 The desktop aardvark is an ornery thing.
138138
139Let's ensure the registrant and maintainer are listed correctly.
140
141 >>> registrant = find_tag_by_id(user_browser.contents,
142 ... 'registration')
143 >>> print extract_text(registrant)
144 Registered...by...No Privileges Person...
145
146 >>> maintainer = find_tag_by_id(user_browser.contents,
147 ... 'owner')
148 >>> print extract_text(maintainer)
149 Maintainer: No Privileges Person...
150
151
152Turning over maintainership
153---------------------------
154
155Sample Person wants to create a project in Launchpad for a project
156that exists elsewhere as an upstream. She wants it to exist in
157Launchpad so she can file a bug, for instance, but she is not
158interested in being the project maintainer for the long run.
159
160 >>> user_browser.open('http://launchpad.dev')
161 >>> user_browser.getLink('Register a project').click()
162
163 >>> user_browser.getControl('Name').value = 'kittyhawk'
164 >>> user_browser.getControl('URL').value = 'kittyhawk'
165 >>> user_browser.getControl('Title').value = 'Kitty Hawk ATC'
166 >>> user_browser.getControl('Summary').value = (
167 ... 'Kitty Hawk Air Traffic Simulator')
168 >>> user_browser.getControl('Continue').click()
169 >>> user_browser.getControl('Python License').click()
170 >>> disclaim = user_browser.getControl(name='field.disclaim_maintainer')
171 >>> disclaim.value = ['checked']
172 >>> user_browser.getControl('Complete Registration').click()
173
174Sample person is shown as the registrant but the maintainer is now
175Registry Admins.
176
177 >>> registrant = find_tag_by_id(user_browser.contents,
178 ... 'registration')
179 >>> print extract_text(registrant)
180 Registered...by...No Privileges Person...
181
182 >>> maintainer = find_tag_by_id(user_browser.contents,
183 ... 'owner')
184 >>> print extract_text(maintainer)
185 Maintainer: Registry Administrators...
186
139187
140Search results188Search results
141--------------189--------------
142190
=== modified file 'lib/lp/registry/stories/product/xx-product-driver.txt'
--- lib/lp/registry/stories/product/xx-product-driver.txt 2009-10-08 16:29:45 +0000
+++ lib/lp/registry/stories/product/xx-product-driver.txt 2010-06-24 02:23:26 +0000
@@ -22,10 +22,6 @@
22 >>> print browser.url22 >>> print browser.url
23 http://launchpad.dev/firefox23 http://launchpad.dev/firefox
2424
25 >>> for tag in find_tags_by_class(browser.contents, 'informational'):
26 ... print tag.renderContents()
27 Successfully changed the driver to Sample Person
28
29Sample Person is listed as the driver of the product.25Sample Person is listed as the driver of the product.
3026
31 >>> main = find_main_content(browser.contents)27 >>> main = find_main_content(browser.contents)