Merge lp:~jtv/launchpad/extract-approval-js into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merged at revision: 11097
Proposed branch: lp:~jtv/launchpad/extract-approval-js
Merge into: lp:launchpad
Diff against target: 323 lines (+171/-86)
4 files modified
lib/lp/app/templates/base-layout-macros.pt (+2/-0)
lib/lp/translations/javascript/importqueueentry.js (+159/-0)
lib/lp/translations/templates/translationimportqueueentry-index.pt (+5/-83)
lib/lp/translations/windmill/tests/test_import_queue.py (+5/-3)
To merge this branch: bzr merge lp:~jtv/launchpad/extract-approval-js
Reviewer Review Type Date Requested Status
Michael Nelson (community) js Approve
Jelmer Vernooij (community) code Approve
Review via email: mp+28852@code.launchpad.net

Commit message

Extract translations import queue entry JS, fix chromium layout problem.

Description of the change

= Translations Import Queue Entry: Bug 589567 and Extraction =

The TAL for the translations import queue entry form contains javascript to make the form adjust dynamically to the choice of file type. Fields that don't apply to the selected file type disappear, and ones that do flash into view as soon as a new type is selected.

One small problem with this is that on Chromium, the fields that disappear continue to take up the same space, making for a very awkward form layout.

In this branch I move that javascript out into a .js file of its own. This opens the door to some improvements I'm planning to facilitate import queue review, as well as (I hope) future unit-testing of its individual functions, and either replacing getElemById with something generic or putting it in a more reusable place. I also took the opportunity to clean things up a bit.

Functionally, nothing changes apart from the fix to bug 589567 (the layout problem in Chromium), and the removal of some CSS classes (unused and undefined) that the JS assigned to all form fields as an implementation detail of its initialization algorithm. I fixed the Chromium issue by replacing the "visibility: collapsed" for hidden form rows with "display: none." For that I used a CSS class we already had: unseen.

On top of this I did make some fairly radical, but semantically neutral changes to how the code finds the DOM nodes it needs to show or hide, and how it shows or hides them.

First of these, the way it finds the nodes. The old code found and cached lists of DOM nodes for each file type as the form first rendered. This was quite complicated, and there were then two separate functions to adjust the form: one assumed that all fields were hidden and the other showed a green flash on fields that it made visible. The former was used for initial display (the node-lists initialization hid all fields as a side effect) and the other handles changes of the selected file type by the user.

The new code throws away all this optimization in favour of simplicity. A single method goes through the lists of relevant fields per file type, hides the irrelevant ones, and shows the relevant ones. The green flash is optional, controlled by an argument. There's no more caching of node lists. Only one function needs to know which fields are visible for which file types. For the predominant usage scenario, where the file type never changes, this is actually a lot less work than the optimized version—but I doubt it matters.

Second, there's how the nodes are manipulated. Instead of trying to do everything in a big smart loop I have several very simple loops iterating over the nodes to do one simple thing. This transformation is known as "loop fission." The loop is a bit complex so I extracted it into a function; the manipulation to be applied to each node (show, hide, animate with green flash) is passed in as another function.

The javascript is covered by a windmill test, which only needed a tiny update to the CSS it looks for to recognize hidden form rows plus some drive-by cleanups. To run the test:
{{{
./bin/test -vv -m lp.translations.windmill -t ImportQueueEntry
}}}

I had some timeouts running this locally but it came through EC2 fine (and amazingly, it did actually run the tests). To run the above on EC2, do:
{{{
./utilities/ec2 test -o "-vv -m lp.translations.windmill -t ImportQueueEntry"
}}}

Q/A takes Rosetta or Launchpad admin rights, or membership of the Ubuntu Translations Coordinator team. Go to https://translations.${YOUR_LP_SERVER}/+imports/ and pick an entry. At the bottom of the entry it should say either "Will be imported into" or "No import target selected yet." If you have the necessary privileges, there will be an "edit" link next to that. Follow it to get to the entry's form.

On the form, verify that only fields relevant to the file's type are shown. For instance, a Translations (PO) file will have a Language field but a Template (POT) will not. On the other hand a Template will have a Translation Domain field which a Translations file doesn't. The fields adjust when you select different file types, with new fields appearing in a green flash.

Jeroen

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

As I mentioned on IRC my JavaScript knowledge is a bit rusty.

I understand what you're doing and as far as I can tell it matches the JavaScript style guide. The tests fail here locally and ec2, which is a bit of a worry.

I would prefer somebody with a bit more experience reviewing Launchpad JavaScript code has a look as well. The fact that the test (spuriously?) fails is a blocker, it would be interesting to know why it is failing on some machines.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

FWIW, the test failure:

Failure in test lp.translations.windmill.tests.test_import_queue.ImportQueueEntryTest.test_import_queue_entry
Traceback (most recent call last):
_StringException: Text attachment: traceback
------------
Traceback (most recent call last):
  File "/var/launchpad/tmp/eggs/testtools-0.9.2-py2.5.egg/testtools/runtest.py", line 128, in _run_user
    return fn(*args)
  File "/var/launchpad/tmp/eggs/testtools-0.9.2-py2.5.egg/testtools/testcase.py", line 368, in _run_test_method
    testMethod()
  File "/var/launchpad/test/lib/lp/translations/windmill/tests/test_import_queue.py", line 91, in test_import_queue_entry
    client.waits.forElement(id=u'field.file_type', timeout=u'8000')
  File "/var/launchpad/tmp/eggs/windmill-1.3beta3_lp_r1440-py2.5.egg/windmill/authoring/__init__.py", line 161, in __call__
    return self.exec_method(self.action_name, **kwargs)
  File "/var/launchpad/tmp/eggs/windmill-1.3beta3_lp_r1440-py2.5.egg/windmill/authoring/__init__.py", line 214, in _exec_test
    raise WindmillTestClientException(result['result'])
WindmillTestClientException: {'suite_name': 'Translations import queue entry', 'result': False, 'starttime': '2010-5-30T23:3:7.392Z', 'params': {'id': 'field.file_type', 'timeout': '8000'}, 'output': None, 'endtime': '2010-5-30T23:3:15.491Z', 'method': 'waits.forElement'}
------------

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Needs Fixing (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Yeah, that's the timeout. I get that for the other windmill tests as well. I thought it was just my local setup (which is why I went to EC2) but perhaps it's more. I'm investigating.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

{{{
16:01 < jelmer> noodles: Could you perhaps also have a quick look at jtv's branch? I haven't reviewed JS code before
16:02 < jelmer> not necessarily extensive but just making sure I haven't missed something obvious
16:08 < jelmer> noodles, MP is here: https://code.edge.launchpad.net/~jtv/launchpad/extract-approval-js/+merge/28852
16:13 < jtv> noodles: I must admit I'm a bit rusty at the JS coding standards myself, so high time I did that cleanup. :)
16:17 < noodles> jtv: me too.. it's been a while.
16:17 < jtv> noodles: or we can just decide that it looks good enough and whatever I did must be the right way. :-)
16:17 < noodles> Yeah, I won't be doing a thorough review... just double checking jelmer's excellent work :)
16:19 < noodles> jtv: how is it that you've got new code (ie. I can't see where it's moved from) with XXX's from henning? ;)
16:20 < noodles> jtv: Ah, you just added henning's name... OK :)
16:20 < jtv> noodles: he wrote it, and I went back to figure out the date so I could meet XXX standards.
16:20 < jtv> "Launchpad, the only major open-source software project with formal XXX standards. Your seal of quality."
16:20 < henninge> jtv: thanks ;)
16:20 < noodles> lol
16:21 * jtv still wants raunchpad.net
16:24 < noodles> jtv, jelmer : Looks great... and yay for future unit tests.
16:24 < jtv> yes, that would be nice wouldn't it?
}}}

Revision history for this message
Jelmer Vernooij (jelmer) :
review: Approve (code)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

I've not done a thorough review, but have just made sure nothing obvious is missing. As in the MP, the branch seems to clean up previous JS - the only behavioral change being the trivial chromium fix. I've not tested the changes locally but have left that to Jelmer.

Lots of nice cleanups and additional doc strings... thanks for the attention to detail (and sorry I can't give it the same attention right now).

review: Approve (js)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
2--- lib/lp/app/templates/base-layout-macros.pt 2010-06-28 13:59:04 +0000
3+++ lib/lp/app/templates/base-layout-macros.pt 2010-07-01 10:46:30 +0000
4@@ -226,6 +226,8 @@
5 <script type="text/javascript"
6 tal:attributes="src string:${lp_js}/translations/importqueue.js"></script>
7 <script type="text/javascript"
8+ tal:attributes="src string:${lp_js}/translations/importqueueentry.js"></script>
9+ <script type="text/javascript"
10 tal:attributes="src string:${lp_js}/translations/languages.js"></script>
11 <script type="text/javascript"
12 tal:attributes="src string:${lp_js}/translations/pofile.js"></script>
13
14=== added file 'lib/lp/translations/javascript/importqueueentry.js'
15--- lib/lp/translations/javascript/importqueueentry.js 1970-01-01 00:00:00 +0000
16+++ lib/lp/translations/javascript/importqueueentry.js 2010-07-01 10:46:30 +0000
17@@ -0,0 +1,159 @@
18+/* Copyright 2010 Canonical Ltd. This software is licensed under the
19+ * GNU Affero General Public License version 3 (see the file LICENSE).
20+ *
21+ * @module lp.translations.importqueueentry
22+ * @requires node, lazr.anim
23+ */
24+
25+YUI.add('lp.translations.importqueueentry', function(Y) {
26+
27+var namespace = Y.namespace('lp.translations.importqueueentry');
28+
29+
30+/**
31+ * Groups of form fields to be visible per file type.
32+ * @private
33+ */
34+var field_groups = {
35+ 'POT': ['name', 'translation_domain', 'languagepack'],
36+ 'PO': ['potemplate', 'potemplate_name', 'language', 'variant'],
37+ 'UNSPEC': []
38+};
39+
40+
41+var last_file_type = 'UNSPEC';
42+
43+var hidden_field_class = 'unseen';
44+
45+
46+/**
47+ * Find a page element by HTML id.
48+ *
49+ * Works around a YUI bug.
50+ *
51+ * @private
52+ * @method getElemById
53+ * @param elem_id The HTML id of an element on the current page.
54+ * @return The DOM Node with that id, or None.
55+ */
56+function getElemById(elem_id) {
57+ /* XXX HenningEggers 2009-03-24: 'elem_id' is a Zope form field, and
58+ * triggers YUI bug #2423101. We'll work around it.
59+ */
60+ return Y.one(Y.DOM.byId(elem_id));
61+}
62+
63+
64+/**
65+ * Find the DOM input element for a given zope field.
66+ * @private
67+ * @method getFormField
68+ * @param field_name Name of a zope form field.
69+ * @return DOM input field corresponding to the zope form field.
70+ */
71+function getFormField(field_name) {
72+ return getElemById('field.' + field_name);
73+}
74+
75+
76+/**
77+ * Find the table row that contains the given form field.
78+ * @private
79+ * @method getFormEntry
80+ * @param field_name Name of a zope form field.
81+ * @return DOM entry containing the entire zope form field including
82+ * markup.
83+ */
84+function getFormEntry(field_name) {
85+ /* We're interested in the tr tag surrounding the input element that
86+ * Zope generated for the field. The input element's id consists of
87+ * the string "field" and the field's name, joined by a dot.
88+ */
89+ var field = getFormField(field_name);
90+ return (field === null) ? null : field.ancestor('tr');
91+}
92+
93+
94+/**
95+ * Apply function `alteration` to the form fields for a given file type.
96+ * @private
97+ * @method alterFields
98+ * @param file_type Apply to fields associated with this file type.
99+ * @param alteration Function to run for each field; takes the field's
100+ * DOM element as an argument.
101+ */
102+function alterFields(file_type, alteration) {
103+ var field_names = field_groups[file_type];
104+ if (field_names !== null) {
105+ Y.Array.each(field_names, function (field_name) {
106+ var tr = getFormEntry(field_name);
107+ if (tr !== null) {
108+ alteration(tr);
109+ }
110+ });
111+ }
112+}
113+
114+
115+/**
116+ * Change selected file type.
117+ * @private
118+ * @param file_type The newly selected file type.
119+ * @param interactively Animate the appareance of previously hidden fields.
120+ */
121+function updateCurrentFileType(file_type, interactively) {
122+ // Hide irrelevant fields.
123+ var hideElement = function (element) {
124+ element.addClass(hidden_field_class);
125+ };
126+ for (var group in field_groups) {
127+ if (group !== file_type) {
128+ alterFields(group, hideElement);
129+ }
130+ }
131+
132+ // Reveal relevant fields.
133+ var showElement = function (element) {
134+ element.removeClass(hidden_field_class);
135+ };
136+ alterFields(file_type, function (element) {
137+ element.removeClass(hidden_field_class);
138+ });
139+
140+ if (interactively) {
141+ // Animate revealed fields.
142+ var animateElement = function (element) {
143+ Y.lazr.anim.green_flash({node: element}).run();
144+ };
145+ alterFields(file_type, animateElement);
146+ }
147+
148+ last_file_type = file_type;
149+}
150+
151+
152+/**
153+ * Handle change event for current file type.
154+ * @private
155+ * @method handleFileTypeChange
156+ */
157+function handleFileTypeChange() {
158+ var file_type = this.get('value');
159+ if (file_type != last_file_type) {
160+ updateCurrentFileType(file_type, true);
161+ }
162+}
163+
164+
165+/**
166+ * Set up the import-queue-entry page.
167+ * @method setup_page
168+ */
169+namespace.setup_page = function () {
170+ var file_type_field = getFormField('file_type');
171+ var preselected_file_type = file_type_field.get('value');
172+ updateCurrentFileType(preselected_file_type, false);
173+ file_type_field.on('change', handleFileTypeChange);
174+};
175+
176+}, "0.1", {"requires": ['node', 'lazr.anim']});
177
178=== modified file 'lib/lp/translations/templates/translationimportqueueentry-index.pt'
179--- lib/lp/translations/templates/translationimportqueueentry-index.pt 2010-02-10 13:04:10 +0000
180+++ lib/lp/translations/templates/translationimportqueueentry-index.pt 2010-07-01 10:46:30 +0000
181@@ -10,92 +10,14 @@
182 <metal:script fill-slot="head_epilogue">
183 <style type="text/css">
184 .dont_show_fields {
185- visibility: collapse;
186+ display: none;
187 }
188 </style>
189 <script type="text/javascript">
190- LPS.use('node', 'lazr.anim', function(Y) {
191- var fields = {'POT':
192- ['field.name', 'field.translation_domain',
193- 'field.languagepack'],
194- 'PO':
195- ['field.potemplate', 'field.potemplate_name',
196- 'field.language', 'field.variant'],
197- 'UNSPEC': []
198- };
199- var nodes = {};
200- var last_file_type = 'UNSPEC';
201-
202- function getElemById(elem_id) {
203- // XXX 'elem_id' is a Zope form field, and triggers
204- // YUI bug #2423101. We'll work around it.
205- return Y.one(Y.DOM.byId(elem_id));
206- }
207-
208- function getEnclosingTR(fieldname) {
209- var field = getElemById(fieldname);
210- if (field == null) {
211- return null;
212- }
213- return field.ancestor('tr');
214- }
215-
216- function buildNodeLists() {
217- for (var ftype in fields) {
218- var the_class = ftype + '_row';
219- Y.Array.each(fields[ftype], function(field) {
220- var tr = getEnclosingTR(field);
221- if (tr != null) {
222- tr.addClass(the_class);
223- tr.addClass('dont_show_fields');
224- }
225- });
226- nodes[ftype] = Y.all('.' + the_class);
227- }
228- }
229-
230- function updateCurrentFileType(file_type) {
231- for (ftype in nodes) {
232- // Logic has been inverted in the next line to avoid
233- // breaking XHTML compliance of the template due to
234- // ampersand usage.
235- if (!(ftype == file_type || nodes[ftype] == null)) {
236- nodes[ftype].addClass('dont_show_fields');
237- }
238- }
239- if (nodes[file_type] != null) {
240- nodes[file_type].removeClass('dont_show_fields');
241- nodes[file_type].each( function(node) {
242- var anim = Y.lazr.anim.green_flash({ node: node });
243- anim.run();
244- });
245- }
246- last_file_type = file_type;
247- }
248-
249- function initCurrentFileType(file_type) {
250- // Same as updateCurrentFileType but without collapsing
251- // everything and without the green flash.
252- if (nodes[file_type] != null) {
253- nodes[file_type].removeClass('dont_show_fields');
254- }
255- last_file_type = file_type;
256- }
257-
258- function handleFileTypeChange() {
259- var file_type = this.get('value');
260- if (file_type != last_file_type) {
261- updateCurrentFileType(file_type);
262- }
263- }
264-
265- Y.on('domready', function(){
266- buildNodeLists();
267- var file_type_field = getElemById('field.file_type');
268- initCurrentFileType(file_type_field.get('value'));
269- file_type_field.on('change', handleFileTypeChange);
270- });
271- });
272+ LPS.use('node', 'lp.translations.importqueueentry',
273+ function (Y) {
274+ Y.on('domready', Y.lp.translations.importqueueentry.setup_page);
275+ });
276 </script>
277 </metal:script>
278
279
280=== modified file 'lib/lp/translations/windmill/tests/test_import_queue.py'
281--- lib/lp/translations/windmill/tests/test_import_queue.py 2010-02-18 10:51:34 +0000
282+++ lib/lp/translations/windmill/tests/test_import_queue.py 2010-07-01 10:46:30 +0000
283@@ -20,6 +20,7 @@
284 from lp.translations.windmill.testing import TranslationsWindmillLayer
285 from lp.testing import WindmillTestCase
286
287+
288 class ImportQueueEntryTest(WindmillTestCase):
289 """Test that the entries in the import queue can switch types."""
290
291@@ -42,13 +43,14 @@
292 'field.potemplate',
293 'field.language',
294 ]
295+
296 def _getHiddenTRXpath(self, field_id):
297 if field_id in self.SELECT_FIELDS:
298 input_tag = 'select'
299 else:
300 input_tag = 'input'
301 return (
302- u"//tr[contains(@class,'dont_show_fields')]"
303+ u"//tr[contains(@class,'unseen')]"
304 u"//%s[@id='%s']" % (input_tag, field_id)
305 )
306
307@@ -87,8 +89,7 @@
308 # the first ones are hidden. Finally, all fields are hidden if the
309 # file type is unspecified.
310 client.waits.forElement(id=u'field.file_type', timeout=u'8000')
311- client.asserts.assertSelected(id=u'field.file_type',
312- validator=u'POT')
313+ client.asserts.assertSelected(id=u'field.file_type', validator=u'POT')
314 self._assertAllFieldsVisible(client, 'POT')
315 self._assertAllFieldsHidden(client, 'PO')
316
317@@ -105,6 +106,7 @@
318 IMPORT_STATUS_1 = IMPORT_STATUS % 1
319 OPEN_CHOICELIST = u"//div[contains(@class, 'yui-ichoicelist-content')]"
320
321+
322 class ImportQueueStatusTest(WindmillTestCase):
323 """Test that the entries in the import queue can switch types."""
324