Merge lp:~thumper/launchpad/choice-widget into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 12567
Proposed branch: lp:~thumper/launchpad/choice-widget
Merge into: lp:launchpad
Diff against target: 448 lines (+180/-124)
11 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+1/-1)
lib/lp/app/browser/lazrjs.py (+37/-0)
lib/lp/app/doc/lazr-js-widgets.txt (+57/-0)
lib/lp/app/javascript/choice.js (+39/-12)
lib/lp/app/templates/enum-choice-widget.pt (+17/-0)
lib/lp/code/browser/branch.py (+5/-10)
lib/lp/code/javascript/branch.status.js (+0/-49)
lib/lp/code/stories/branches/xx-branch-edit.txt (+1/-1)
lib/lp/code/templates/branch-index.pt (+0/-2)
lib/lp/code/templates/branch-information.pt (+1/-9)
lib/lp/code/windmill/tests/test_branch_status.py (+22/-40)
To merge this branch: bzr merge lp:~thumper/launchpad/choice-widget
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+52357@code.launchpad.net

Commit message

[r=gmb][bug=731629] Create an EnumChoiceWidget and use it on the branch page.

Description of the change

Call this a tech-debt removal branch, in that we have too much
specific javascript for simply updating an enum field.

A new widget is created to our bucket of widgets: EnumChoiceWidget.

As a proof of concept, the branch status update is changed to
use this new widget.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
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/icing/style-3-0.css.in'
2--- lib/canonical/launchpad/icing/style-3-0.css.in 2011-03-03 18:56:00 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-03-09 21:29:50 +0000
4@@ -1618,7 +1618,7 @@
5 }
6 .branchstatusMERGED, .branchstatusMERGED a,
7 .branchstatusABANDONED, .branchstatusABANDONED a {
8- color: #gray;
9+ color: gray;
10 }
11 .branchstatusNEW, .branchstatusNEW a {
12 color: black;
13
14=== modified file 'lib/lp/app/browser/lazrjs.py'
15--- lib/lp/app/browser/lazrjs.py 2011-02-10 01:57:55 +0000
16+++ lib/lp/app/browser/lazrjs.py 2011-03-09 21:29:50 +0000
17@@ -6,6 +6,7 @@
18 __metaclass__ = type
19 __all__ = [
20 'BooleanChoiceWidget',
21+ 'EnumChoiceWidget',
22 'InlineEditPickerWidget',
23 'standard_text_html_representation',
24 'TextAreaEditorWidget',
25@@ -21,6 +22,7 @@
26 from zope.schema.interfaces import IVocabulary
27 from zope.schema.vocabulary import getVocabularyRegistry
28
29+from lazr.enum import IEnumeratedType
30 from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
31 from canonical.lazr.utils import get_current_browser_request
32 from canonical.lazr.utils import safe_hasattr
33@@ -406,3 +408,38 @@
34 @property
35 def json_config(self):
36 return simplejson.dumps(self.config)
37+
38+
39+class EnumChoiceWidget(EditableWidgetBase):
40+ """A popup choice widget."""
41+
42+ __call__ = ViewPageTemplateFile('../templates/enum-choice-widget.pt')
43+
44+ def __init__(self, context, exported_field, header,
45+ content_box_id=None, enum=None,
46+ edit_view="+edit", edit_url=None,
47+ css_class_prefix=''):
48+ super(EnumChoiceWidget, self).__init__(
49+ context, exported_field, content_box_id, edit_view, edit_url)
50+ self.header = header
51+ value = getattr(self.context, self.attribute_name)
52+ self.css_class = "value %s%s" % (css_class_prefix, value.name)
53+ self.value = value.title
54+ if enum is None:
55+ # Get the enum from the exported field.
56+ enum = exported_field.vocabulary
57+ if IEnumeratedType(enum, None) is None:
58+ raise ValueError('%r does not provide IEnumeratedType' % enum)
59+ self.items = vocabulary_to_choice_edit_items(enum, css_class_prefix)
60+
61+ @property
62+ def config(self):
63+ return dict(
64+ contentBox='#'+self.content_box_id,
65+ value=self.value,
66+ title=self.header,
67+ items=self.items)
68+
69+ @property
70+ def json_config(self):
71+ return simplejson.dumps(self.config)
72
73=== modified file 'lib/lp/app/doc/lazr-js-widgets.txt'
74--- lib/lp/app/doc/lazr-js-widgets.txt 2011-03-01 04:48:10 +0000
75+++ lib/lp/app/doc/lazr-js-widgets.txt 2011-03-09 21:29:50 +0000
76@@ -326,3 +326,60 @@
77
78 The edit link can be changed in exactly the same way as for the
79 TextLineEditorWidget above.
80+
81+
82+EnumChoiceWidget
83+----------------
84+
85+This widget is primarily there allow setting of enum fields easily
86+using the popup chooser.
87+
88+ >>> from lp.app.browser.lazrjs import EnumChoiceWidget
89+
90+As with the other widgets, this one requires a context object and a Choice type
91+field. The rendering of the widget hooks up to the lazr ChoiceSource with the
92+standard patch plugin.
93+
94+One of the different things about this widget is the styles that are added.
95+Many enums have specific colour styles. Generally these are the names of
96+the enum values (the upper-case bit) with a defined prefix. This prefix
97+is passed in to the constructor of the widget.
98+
99+A list of items is generated from the vocabulary.
100+
101+If the user does not have edit rights, the widget just renders the text based
102+on the current value of the field on the object:
103+
104+ >>> login(ANONYMOUS)
105+ >>> from lp.code.interfaces.branch import IBranch
106+ >>> branch = factory.makeAnyBranch()
107+ >>> widget = EnumChoiceWidget(
108+ ... branch, IBranch['lifecycle_status'],
109+ ... header='Change status to', css_class_prefix='branchstatus')
110+ >>> print widget()
111+ <span id="edit-lifecycle_status">
112+ <span class="value branchstatusDEVELOPMENT">Development</span>
113+ </span>
114+
115+If the user has edit rights, an edit icon is rendered and some javascript is
116+rendered to hook up the widget.
117+
118+ >>> login_person(branch.owner)
119+ >>> print widget()
120+ <span id="edit-lifecycle_status">
121+ <span class="value branchstatusDEVELOPMENT">Development</span>
122+ <span>&nbsp;
123+ <a class="editicon sprite edit" href="http://.../+edit"></a>
124+ </span>
125+ </span>
126+ <script>
127+ LPS.use('lp.app.choice', function(Y) {
128+ ...
129+ </script>
130+
131+
132+Changing the edit link
133+**********************
134+
135+The edit link can be changed in exactly the same way as for the
136+TextLineEditorWidget above.
137
138=== modified file 'lib/lp/app/javascript/choice.js'
139--- lib/lp/app/javascript/choice.js 2011-02-14 00:44:44 +0000
140+++ lib/lp/app/javascript/choice.js 2011-03-09 21:29:50 +0000
141@@ -2,18 +2,7 @@
142
143 var namespace = Y.namespace('lp.app.choice');
144
145-namespace.addBinaryChoice = function(config, resource_uri, attribute) {
146-
147- if (Y.UA.ie) {
148- return;
149- }
150-
151- var widget = new Y.ChoiceSource(config);
152- widget.plug({
153- fn: Y.lp.client.plugins.PATCHPlugin,
154- cfg: {
155- patch: attribute,
156- resource: resource_uri}});
157+function hook_up_spinner(widget) {
158 // ChoiceSource makes assumptions about HTML in lazr-js
159 // that don't hold true here, so we need to do our own
160 // spinner icon and clear it when finished.
161@@ -30,8 +19,46 @@
162 icon.addClass('edit');
163 icon.setStyle('bottom', '0px');
164 }, widget, '_uiClearWaiting');
165+}
166+
167+namespace.addBinaryChoice = function(config, resource_uri, attribute) {
168+
169+ if (Y.UA.ie) {
170+ return;
171+ }
172+
173+ var widget = new Y.ChoiceSource(config);
174+ widget.plug({
175+ fn: Y.lp.client.plugins.PATCHPlugin,
176+ cfg: {
177+ patch: attribute,
178+ resource: resource_uri}});
179+ hook_up_spinner(widget);
180 widget.render();
181 };
182
183
184+namespace.addEnumChoice = function(config, resource_uri, attribute) {
185+
186+ var widget = new Y.ChoiceSource(config);
187+ widget.plug({
188+ fn: Y.lp.client.plugins.PATCHPlugin,
189+ cfg: {
190+ patch: attribute,
191+ resource: resource_uri}});
192+ hook_up_spinner(widget);
193+ widget.on('save', function(e) {
194+ var cb = widget.get('contentBox');
195+ var value = widget.get('value');
196+ Y.Array.each(config.items, function(item) {
197+ if (item.value == value) {
198+ cb.one('span').addClass(item.css_class);
199+ } else {
200+ cb.one('span').removeClass(item.css_class);
201+ }
202+ });
203+ });
204+ widget.render();
205+}
206+
207 }, "0.1", {"requires": ["lazr.choiceedit", "lp.client.plugins"]});
208
209=== added file 'lib/lp/app/templates/enum-choice-widget.pt'
210--- lib/lp/app/templates/enum-choice-widget.pt 1970-01-01 00:00:00 +0000
211+++ lib/lp/app/templates/enum-choice-widget.pt 2011-03-09 21:29:50 +0000
212@@ -0,0 +1,17 @@
213+<span tal:attributes="id view/content_box_id">
214+ <span tal:attributes="class view/css_class"
215+ tal:content="structure view/value" />
216+ <span tal:condition="view/can_write">&nbsp;
217+ <a tal:attributes="href view/edit_url"
218+ class="editicon sprite edit"></a>
219+ </span>
220+</span>
221+<script tal:condition="view/can_write"
222+ tal:content="structure string:
223+LPS.use('lp.app.choice', function(Y) {
224+ Y.lp.app.choice.addEnumChoice(
225+ ${view/json_config},
226+ ${view/json_resource_uri},
227+ ${view/json_attribute});
228+});
229+"/>
230
231=== modified file 'lib/lp/code/browser/branch.py'
232--- lib/lp/code/browser/branch.py 2011-03-03 05:18:26 +0000
233+++ lib/lp/code/browser/branch.py 2011-03-09 21:29:50 +0000
234@@ -107,6 +107,7 @@
235 )
236 from lp.app.browser.lazrjs import vocabulary_to_choice_edit_items
237 from lp.app.errors import NotFoundError
238+from lp.app.browser.lazrjs import EnumChoiceWidget
239 from lp.app.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
240 from lp.app.widgets.suggestion import TargetBranchWidget
241 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
242@@ -668,17 +669,11 @@
243 return list(self.context.getProductSeriesPushingTranslations())
244
245 @property
246- def status_config(self):
247+ def status_widget(self):
248 """The config to configure the ChoiceSource JS widget."""
249- return simplejson.dumps({
250- 'status_widget_items': vocabulary_to_choice_edit_items(
251- BranchLifecycleStatus,
252- css_class_prefix='branchstatus'),
253- 'status_value': self.context.lifecycle_status.title,
254- 'user_can_edit_status': check_permission(
255- 'launchpad.Edit', self.context),
256- 'branch_path': '/' + self.context.unique_name,
257- })
258+ return EnumChoiceWidget(
259+ self.context.branch, IBranch['lifecycle_status'],
260+ header='Change status to', css_class_prefix='branchstatus')
261
262
263 class BranchInProductView(BranchView):
264
265=== removed file 'lib/lp/code/javascript/branch.status.js'
266--- lib/lp/code/javascript/branch.status.js 2010-07-15 10:55:27 +0000
267+++ lib/lp/code/javascript/branch.status.js 1970-01-01 00:00:00 +0000
268@@ -1,49 +0,0 @@
269-/* Copyright 2009 Canonical Ltd. This software is licensed under the
270- * GNU Affero General Public License version 3 (see the file LICENSE).
271- *
272- * Code for handling the update of the branch status.
273- *
274- * @module lp.code.branchstatus
275- * @requires node, lazr.choiceedit, lp.client.plugins
276- */
277-
278-YUI.add('lp.code.branch.status', function(Y) {
279-
280-var namespace = Y.namespace('lp.code.branch.status');
281-
282-/*
283- * Connect the branch status to the javascript events.
284- */
285-namespace.connect_status = function(conf) {
286-
287- var status_content = Y.one('#branch-details-status-value');
288-
289- if (conf.user_can_edit_status) {
290- var status_choice_edit = new Y.ChoiceSource({
291- contentBox: status_content,
292- value: conf.status_value,
293- title: 'Change status to',
294- items: conf.status_widget_items});
295- status_choice_edit.showError = function(err) {
296- Y.lp.app.errors.display_error(null, err);
297- };
298- status_choice_edit.on('save', function(e) {
299- var cb = status_choice_edit.get('contentBox');
300- Y.Array.each(conf.status_widget_items, function(item) {
301- if (item.value == status_choice_edit.get('value')) {
302- cb.one('span').addClass(item.css_class);
303- } else {
304- cb.one('span').removeClass(item.css_class);
305- }
306- });
307- });
308- status_choice_edit.plug({
309- fn: Y.lp.client.plugins.PATCHPlugin,
310- cfg: {
311- patch: 'lifecycle_status',
312- resource: conf.branch_path}});
313- status_choice_edit.render();
314- }
315-};
316-
317-}, "0.1", {"requires": ["node", "lazr.choiceedit", "lp.client.plugins"]});
318
319=== modified file 'lib/lp/code/stories/branches/xx-branch-edit.txt'
320--- lib/lp/code/stories/branches/xx-branch-edit.txt 2010-10-03 15:30:06 +0000
321+++ lib/lp/code/stories/branches/xx-branch-edit.txt 2011-03-09 21:29:50 +0000
322@@ -111,7 +111,7 @@
323 'http://code.launchpad.dev/~name12/gnome-terminal/klingon'
324
325 >>> contents = browser.contents
326- >>> status_tag = find_tag_by_id(contents, 'branch-details-status-value')
327+ >>> status_tag = find_tag_by_id(contents, 'edit-lifecycle_status')
328 >>> print extract_text(status_tag)
329 Merged
330
331
332=== modified file 'lib/lp/code/templates/branch-index.pt'
333--- lib/lp/code/templates/branch-index.pt 2011-02-23 22:32:15 +0000
334+++ lib/lp/code/templates/branch-index.pt 2011-03-09 21:29:50 +0000
335@@ -43,8 +43,6 @@
336 contentBox: '#portlet-subscribers'
337 });
338 subscription_portlet.render();
339-
340- Y.lp.code.branch.status.connect_status(${view/status_config});
341 }
342 Y.lp.code.branchmergeproposal.diff.connect_diff_links();
343 }, window);
344
345=== modified file 'lib/lp/code/templates/branch-information.pt'
346--- lib/lp/code/templates/branch-information.pt 2010-10-19 02:20:08 +0000
347+++ lib/lp/code/templates/branch-information.pt 2011-03-09 21:29:50 +0000
348@@ -22,15 +22,7 @@
349
350 <dl id="status">
351 <dt>Status:</dt>
352- <dd>
353- <span id="branch-details-status-value">
354- <span tal:attributes="class string:value branchstatus${context/lifecycle_status/name}"
355- tal:content="structure context/lifecycle_status/title" />&nbsp;
356- <a href="+edit-status" tal:condition="context/required:launchpad.Edit">
357- <img class="editicon" src="/@@/edit"/>
358- </a>
359- </span>
360- </dd>
361+ <dd tal:content="structure view/status_widget" />
362 </dl>
363 </div>
364
365
366=== modified file 'lib/lp/code/windmill/tests/test_branch_status.py'
367--- lib/lp/code/windmill/tests/test_branch_status.py 2011-02-10 04:00:00 +0000
368+++ lib/lp/code/windmill/tests/test_branch_status.py 2011-03-09 21:29:50 +0000
369@@ -6,19 +6,15 @@
370 __metaclass__ = type
371 __all__ = []
372
373-import unittest
374-
375 import transaction
376-import windmill
377-
378+
379+from storm.store import Store
380+
381+from lp.code.enums import BranchLifecycleStatus
382+from lp.code.model.branch import Branch
383 from lp.code.windmill.testing import CodeWindmillLayer
384 from lp.testing import WindmillTestCase
385-from lp.testing.windmill.constants import (
386- FOR_ELEMENT,
387- PAGE_LOAD,
388- SLEEP,
389- )
390-from lp.testing.windmill.lpuser import login_person
391+from lp.testing.windmill.constants import FOR_ELEMENT
392
393
394 class TestBranchStatus(WindmillTestCase):
395@@ -37,37 +33,23 @@
396
397 client = self.client
398
399- start_url = (
400- windmill.settings['TEST_URL'] + branch.unique_name)
401- client.open(url=start_url)
402- client.waits.forPageLoad(timeout=PAGE_LOAD)
403- login_person(eric, "eric@example.com", "test", client)
404-
405+ client, start_url = self.getClientFor(branch, user=eric)
406 # Click on the element containing the branch status.
407- client.waits.forElement(
408- id=u'branch-details-status-value', timeout=PAGE_LOAD)
409- client.click(id=u'branch-details-status-value')
410- client.waits.forElement(
411- xpath=u'//div[contains(@class, "yui3-ichoicelist-content")]')
412-
413- # Change the status to experimental.
414+ client.click(id=u'edit-lifecycle_status')
415+ client.waits.forElement(
416+ classname=u'yui3-ichoicelist-content', timeout=FOR_ELEMENT)
417 client.click(link=u'Experimental')
418- client.waits.sleep(milliseconds=SLEEP)
419-
420- client.asserts.assertText(
421- xpath=u'//span[@id="branch-details-status-value"]/span',
422- validator=u'Experimental')
423-
424- # Reload the page and make sure the change sticks.
425- client.open(url=start_url)
426- client.waits.forPageLoad(timeout=PAGE_LOAD)
427 client.waits.forElement(
428- xpath=u'//span[@id="branch-details-status-value"]/span',
429+ jquery=u'("div#edit-lifecycle_status a.editicon.sprite.edit")',
430 timeout=FOR_ELEMENT)
431- client.asserts.assertText(
432- xpath=u'//span[@id="branch-details-status-value"]/span',
433- validator=u'Experimental')
434-
435-
436-def test_suite():
437- return unittest.TestLoader().loadTestsFromName(__name__)
438+ client.asserts.assertTextIn(
439+ id=u'edit-lifecycle_status', validator=u'Experimental')
440+ client.asserts.assertNode(
441+ jquery=u'("div#edit-lifecycle_status span.value.branchstatusEXPERIMENTAL")')
442+
443+ transaction.commit()
444+ freshly_fetched_branch = Store.of(branch).find(
445+ Branch, Branch.id == branch.id).one()
446+ self.assertEqual(
447+ BranchLifecycleStatus.EXPERIMENTAL,
448+ freshly_fetched_branch.lifecycle_status)