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