← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/refactor-picker-template into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/refactor-picker-template into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #761331 in Launchpad itself: "Incorrect non-ajax url for vocabulary popup pickers"
  https://bugs.launchpad.net/launchpad/+bug/761331

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/refactor-picker-template/+merge/57804

When a popup picker widget is rendered, you get a text field for direct entry of the value plus a link to invoke the popup to choose a value from a defined vocabulary. Where javascript is not available, there is supposed to be a non-ajax link that is rendered. This link is hard coded to "/people/" which is correct for a people picker but incorrect for a bug tracker picker for example. The code which renders the link needs to use the nonajax_uri property for the link.

== Pre-Implementation ==

I spoke with Curtis about the refactoring included in this mp and we agreed it would be good to do.

== Implementation ==

As well as fixing the bug, I refactored the popup picker implementation to conform with how other widgets are written in lp. That is, each widget has a page template file with javascript invoked by the domready event to wire up the javascript/ajax stuff. Previously, the popup widgets used a vocabulary.js.template file which had values substituted into it and the resulting javascript returned as part of the chooseLink() property. Now everything is done the same way and is easier to understand. 

== Tests ==

Refactored the TestVocabularyPickerWidget.test_js_template_args() test. Renamed to test_widget_template_properties()

 test -vvt TestVocabularyPickerWidget
 test -vvt TestInlineEditPickerWidget

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/configure.zcml
  lib/lp/app/widgets/configure.zcml
  lib/lp/app/widgets/popup.py
  lib/lp/app/widgets/templates/bugtracker-picker.pt
  lib/lp/app/widgets/templates/form-picker-macros.pt
  lib/lp/app/widgets/templates/form-picker.pt
  lib/lp/app/widgets/tests/test_popup.py
  lib/lp/bugs/browser/bugtarget.py

-- 
https://code.launchpad.net/~wallyworld/launchpad/refactor-picker-template/+merge/57804
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/refactor-picker-template into lp:launchpad.
=== modified file 'lib/lp/app/configure.zcml'
--- lib/lp/app/configure.zcml	2011-02-24 15:30:16 +0000
+++ lib/lp/app/configure.zcml	2011-04-15 03:03:34 +0000
@@ -11,4 +11,5 @@
     i18n_domain="launchpad">
     <include package=".browser"/>
     <include package="lp.app.validators" />
+    <include package="lp.app.widgets" />
 </configure>

=== modified file 'lib/lp/app/widgets/configure.zcml'
--- lib/lp/app/widgets/configure.zcml	2011-02-01 21:03:45 +0000
+++ lib/lp/app/widgets/configure.zcml	2011-04-15 03:03:34 +0000
@@ -20,4 +20,10 @@
         permission="zope.Public"
         />
 
+    <browser:page
+        for="*"
+        name="form-picker-macros"
+        permission="zope.Public"
+        template="templates/form-picker-macros.pt"/>
+
 </configure>

=== modified file 'lib/lp/app/widgets/popup.py'
--- lib/lp/app/widgets/popup.py	2011-02-09 15:25:57 +0000
+++ lib/lp/app/widgets/popup.py	2011-04-15 03:03:34 +0000
@@ -8,7 +8,6 @@
 __metaclass__ = type
 
 import cgi
-import os
 
 import simplejson
 from z3c.ptcompat import ViewPageTemplateFile
@@ -110,7 +109,7 @@
         :return: A string that will be passed to Y.Node.create()
                  so it needs to be contained in a single HTML element.
         """
-        return None
+        return simplejson.dumps(None)
 
     @property
     def vocabulary_name(self):
@@ -127,43 +126,26 @@
                 % (choice.context, choice.__name__))
         return choice.vocabularyName
 
-    def js_template_args(self):
-        """return a dict of args to configure the picker javascript."""
-        if self.header is None:
-            header = self.vocabulary.displayname
-        else:
-            header = self.header
-
-        if self.step_title is None:
-            step_title = self.vocabulary.step_title
-        else:
-            step_title = self.step_title
-
-        return dict(
-            vocabulary=self.vocabulary_name,
-            header=header,
-            step_title=step_title,
-            show_widget_id=self.show_widget_id,
-            input_id=self.name,
-            extra_no_results_message=self.extra_no_results_message)
+    @property
+    def header_text(self):
+        return simplejson.dumps(self.header or self.vocabulary.displayname)
+
+    @property
+    def step_title_text(self):
+        return simplejson.dumps(self.step_title or self.vocabulary.step_title)
+
+    @property
+    def input_id(self):
+        return self.name
 
     def chooseLink(self):
-        js_file = os.path.join(os.path.dirname(__file__),
-                               'templates/vocabulary-picker.js.template')
-        js_template = open(js_file).read()
-        args = self.js_template_args()
-        js = js_template % simplejson.dumps(args)
-        # If the YUI widget or javascript is not supported in the browser,
-        # it will degrade to being this "Find..." link instead of the
-        # "Choose..." link. This only works if a non-AJAX form is available
-        # for the field's vocabulary.
         if self.nonajax_uri is None:
             css = 'unseen'
         else:
             css = ''
-        return ('<span class="%s">(<a id="%s" href="/people/">'
-                'Find&hellip;</a>)</span>'
-                '\n<script>\n%s\n</script>') % (css, self.show_widget_id, js)
+        return ('<span class="%s">(<a id="%s" href="%s">'
+                'Find&hellip;</a>)</span>') % (
+            css, self.show_widget_id, self.nonajax_uri or '#')
 
     @property
     def nonajax_uri(self):
@@ -190,48 +172,18 @@
 
 
 class BugTrackerPickerWidget(VocabularyPickerWidget):
+
+    __call__ = ViewPageTemplateFile('templates/bugtracker-picker.pt')
+
     link_template = """
-        or (<a id="%(activator_id)s" href="/bugs/bugtrackers/+newbugtracker"
-                    >Register an external bug tracker&hellip;</a>)
-        <script>
-        LPS.use('lp.bugs.bugtracker_overlay', function(Y) {
-            if (Y.UA.ie) {
-                return;
-            }
-            Y.on('domready', function () {
-                // After the success handler finishes, it calls the
-                // next_step function.
-                var next_step = function(bug_tracker) {
-                    // Fill in the text field with either the name of
-                    // the newly created bug tracker or the name of an
-                    // existing bug tracker whose base_url matches.
-                    var bugtracker_text_box = Y.one(
-                        Y.DOM.byId('field.bugtracker.bugtracker'));
-                    if (bugtracker_text_box !== null) {
-                        bugtracker_text_box.set(
-                            'value', bug_tracker.get('name'));
-                        // It doesn't appear possible to use onChange
-                        // event, so the onKeyPress event is explicitely
-                        // fired here.
-                        if (bugtracker_text_box.get('onkeypress')) {
-                            bugtracker_text_box.get('onkeypress')();
-                        }
-                        bugtracker_text_box.scrollIntoView();
-                    }
-                }
-                Y.lp.bugs.bugtracker_overlay.attach_widget({
-                    activate_node: Y.one('#%(activator_id)s'),
-                    next_step: next_step
-                    });
-                });
-        });
-        </script>
+        or (<a id="create-bugtracker-link"
+        href="/bugs/bugtrackers/+newbugtracker"
+        >Register an external bug tracker&hellip;</a>)
         """
 
     def chooseLink(self):
         link = super(BugTrackerPickerWidget, self).chooseLink()
-        link += self.link_template % dict(
-            activator_id='create-bugtracker-link')
+        link += self.link_template
         return link
 
     @property
@@ -249,6 +201,7 @@
 
     @property
     def extra_no_results_message(self):
-        return ("<strong>Didn't find the project you were looking for? "
+        return simplejson.dumps("<strong>Didn't find the project you were "
+                "looking for? "
                 '<a href="%s/+affects-new-product">Register it</a>.</strong>'
                 % canonical_url(self.context.context))

=== added file 'lib/lp/app/widgets/templates/bugtracker-picker.pt'
--- lib/lp/app/widgets/templates/bugtracker-picker.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/app/widgets/templates/bugtracker-picker.pt	2011-04-15 03:03:34 +0000
@@ -0,0 +1,41 @@
+<tal:root
+    xmlns:tal="http://xml.zope.org/namespaces/tal";
+    xmlns:metal="http://xml.zope.org/namespaces/metal";
+    omit-tag="">
+
+<metal:form-picker use-macro="context/@@form-picker-macros/form-picker"/>
+<script tal:content="structure string:
+LPS.use('lp.bugs.bugtracker_overlay', function(Y) {
+    if (Y.UA.ie) {
+        return;
+    }
+    Y.on('domready', function () {
+        // After the success handler finishes, it calls the
+        // next_step function.
+        var next_step = function(bug_tracker) {
+            // Fill in the text field with either the name of
+            // the newly created bug tracker or the name of an
+            // existing bug tracker whose base_url matches.
+            var bugtracker_text_box = Y.one(
+                Y.DOM.byId('field.bugtracker.bugtracker'));
+            if (bugtracker_text_box !== null) {
+                bugtracker_text_box.set(
+                    'value', bug_tracker.get('name'));
+                // It doesn't appear possible to use onChange
+                // event, so the onKeyPress event is explicitely
+                // fired here.
+                if (bugtracker_text_box.get('onkeypress')) {
+                    bugtracker_text_box.get('onkeypress')();
+                }
+                bugtracker_text_box.scrollIntoView();
+            }
+        }
+        Y.lp.bugs.bugtracker_overlay.attach_widget({
+            activate_node: Y.one('#create-bugtracker-link'),
+            next_step: next_step
+            });
+        });
+});
+"/>
+
+</tal:root>

=== added file 'lib/lp/app/widgets/templates/form-picker-macros.pt'
--- lib/lp/app/widgets/templates/form-picker-macros.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/app/widgets/templates/form-picker-macros.pt	2011-04-15 03:03:34 +0000
@@ -0,0 +1,72 @@
+<tal:root
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  omit-tag="">
+
+  <metal:form-picker define-macro="form-picker">
+    <tal:input replace="structure view/inputField" />
+
+    <tal:search_results tal:condition="not: view/hasValidInput">
+      <select tal:condition="view/matches">
+        <option
+            tal:repeat="match view/matches"
+            tal:attributes="value match/token;
+                selected python:path('match/token') == path('view/formToken');
+                onclick string:this.form['${view/name}'].value = this.value"
+            tal:content="string:${match/token}: ${match/title}"
+            />
+      </select>
+    </tal:search_results>
+
+    <tal:chooseLink replace="structure view/chooseLink" />
+    <script tal:content="structure string:
+    LPS.use('node', 'lp.app.picker', 'plugin', function(Y) {
+        if (Y.UA.ie) {
+            return;
+        }
+
+        // The vocabulary picker, created when used for the first time.
+        var picker = null;
+        function make_picker() {
+            var config = {
+                header: ${view/header_text},
+                step_title: ${view/step_title_text},
+                extra_no_results_message: ${view/extra_no_results_message}
+            };
+            var picker = Y.lp.app.picker.create('${view/vocabulary_name}', config);
+            if (config.extra_no_results_message !== null) {
+                picker.before('resultsChange', function (e) {
+                    var new_results = e.details[0].newVal;
+                    if (new_results.length === 0) {
+                        picker.set('footer_slot',
+                                   Y.Node.create(config.extra_no_results_message));
+                    }
+                    else {
+                        picker.set('footer_slot', null);
+                    }
+                });
+            }
+            picker.plug(Y.lazr.TextFieldPickerPlugin,
+                        {input_element: '[id=\x22${view/input_id}\x22]'});
+            return picker;
+        }
+
+        Y.on('domready', function(e) {
+            // Sort out the Choose... link.
+            var show_widget_node = Y.one('#${view/show_widget_id}');
+
+            show_widget_node.set('innerHTML', 'Choose&hellip;');
+            show_widget_node.addClass('js-action');
+            show_widget_node.get('parentNode').removeClass('unseen');
+            show_widget_node.on('click', function (e) {
+                if (picker === null) {
+                    picker = make_picker();
+                }
+                picker.show();
+                e.preventDefault();
+            });
+        });
+    });
+    "/>
+  </metal:form-picker>
+</tal:root>

=== modified file 'lib/lp/app/widgets/templates/form-picker.pt'
--- lib/lp/app/widgets/templates/form-picker.pt	2009-07-30 19:03:57 +0000
+++ lib/lp/app/widgets/templates/form-picker.pt	2011-04-15 03:03:34 +0000
@@ -1,18 +1,8 @@
-<tal:root xmlns:tal="http://xml.zope.org/namespaces/tal";
-          omit-tag="">
-<tal:input replace="structure view/inputField" />
-
-<tal:search_results tal:condition="not: view/hasValidInput">
-  <select tal:condition="view/matches">
-    <option
-        tal:repeat="match view/matches"
-        tal:attributes="value match/token;
-            selected python:path('match/token') == path('view/formToken');
-            onclick string:this.form['${view/name}'].value = this.value"
-        tal:content="string:${match/token}: ${match/title}"
-        />
-  </select>
-</tal:search_results>
-
-<tal:chooseLink replace="structure view/chooseLink" />
+<tal:root
+    xmlns:tal="http://xml.zope.org/namespaces/tal";
+    xmlns:metal="http://xml.zope.org/namespaces/metal";
+    omit-tag="">
+
+<metal:form-picker use-macro="context/@@form-picker-macros/form-picker"/>
+
 </tal:root>

=== removed file 'lib/lp/app/widgets/templates/vocabulary-picker.js.template'
--- lib/lp/app/widgets/templates/vocabulary-picker.js.template	2011-02-03 21:08:23 +0000
+++ lib/lp/app/widgets/templates/vocabulary-picker.js.template	1970-01-01 00:00:00 +0000
@@ -1,47 +0,0 @@
-LPS.use('node', 'lp.app.picker', 'plugin', function(Y) {
-    if (Y.UA.ie) {
-        return;
-    }
-
-    // Args from python.
-    var args = %s;
-
-    // The vocabulary picker, created when used for the first time.
-    var picker = null;
-    function make_picker() {
-        var config = {
-            header: args.header,
-            step_title: args.step_title,
-            extra_no_results_message: args.extra_no_results_message
-        };
-        var picker = Y.lp.app.picker.create(args.vocabulary, config);
-        if (config.extra_no_results_message !== null) {
-            picker.before('resultsChange', function (e) {
-                var new_results = e.details[0].newVal;
-                if (new_results.length === 0) {
-                    picker.set('footer_slot',
-                               Y.Node.create(config.extra_no_results_message));
-                }
-                else {
-                    picker.set('footer_slot', null);
-                }
-            });
-        }
-        picker.plug(Y.lazr.TextFieldPickerPlugin,
-                    {input_element: '[id="' + args.input_id + '"]'});
-        return picker;
-    }
-
-    // Sort out the "Choose..." link.
-    var show_widget_node = Y.one('#' + args.show_widget_id);
-    show_widget_node.set('innerHTML', 'Choose&hellip;');
-    show_widget_node.addClass('js-action');
-    show_widget_node.get('parentNode').removeClass('unseen');
-    show_widget_node.on('click', function (e) {
-        if (picker === null) {
-            picker = make_picker();
-        }
-        picker.show();
-        e.preventDefault();
-    });
-});

=== modified file 'lib/lp/app/widgets/tests/test_popup.py'
--- lib/lp/app/widgets/tests/test_popup.py	2011-02-02 15:43:31 +0000
+++ lib/lp/app/widgets/tests/test_popup.py	2011-04-15 03:03:34 +0000
@@ -3,6 +3,7 @@
 
 __metaclass__ = type
 
+import simplejson
 from zope.schema.vocabulary import getVocabularyRegistry
 
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
@@ -25,19 +26,20 @@
         self.vocabulary = vocabulary_registry.get(context, 'ValidTeamOwner')
         self.request = LaunchpadTestRequest()
 
-    def test_js_template_args(self):
+    def test_widget_template_properties(self):
         picker_widget = VocabularyPickerWidget(
             self.bound_field, self.vocabulary, self.request)
-        js_template_args = picker_widget.js_template_args()
-        self.assertEqual(
-            'ValidTeamOwner', js_template_args['vocabulary'])
-        self.assertEqual(
-            self.vocabulary.displayname, js_template_args['header'])
-        self.assertEqual(
-            self.vocabulary.step_title, js_template_args['step_title'])
-        self.assertEqual(
-            'show-widget-field-teamowner', js_template_args['show_widget_id'])
-        self.assertEqual(
-            'field.teamowner', js_template_args['input_id'])
-        self.assertEqual(
-            None, js_template_args['extra_no_results_message'])
+        self.assertEqual(
+            'ValidTeamOwner', picker_widget.vocabulary_name)
+        self.assertEqual(
+            simplejson.dumps(self.vocabulary.displayname),
+            picker_widget.header_text)
+        self.assertEqual(
+            simplejson.dumps(self.vocabulary.step_title),
+            picker_widget.step_title_text)
+        self.assertEqual(
+            'show-widget-field-teamowner', picker_widget.show_widget_id)
+        self.assertEqual(
+            'field.teamowner', picker_widget.input_id)
+        self.assertEqual(
+            simplejson.dumps(None), picker_widget.extra_no_results_message)