Merge lp:~wallyworld/launchpad/inline-multicheckbox-widget into lp:launchpad
- inline-multicheckbox-widget
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Aaron Bentley |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12646 |
Proposed branch: | lp:~wallyworld/launchpad/inline-multicheckbox-widget |
Merge into: | lp:launchpad |
Diff against target: |
885 lines (+803/-2) 9 files modified
lib/canonical/launchpad/icing/style-3-0.css.in (+7/-0) lib/lp/app/browser/lazrjs.py (+113/-1) lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py (+82/-0) lib/lp/app/doc/lazr-js-widgets.txt (+92/-0) lib/lp/app/javascript/multicheckbox.js (+232/-0) lib/lp/app/javascript/tests/test_multicheckboxwidget.html (+42/-0) lib/lp/app/javascript/tests/test_multicheckboxwidget.js (+184/-0) lib/lp/app/templates/inline-multicheckbox-widget.pt (+50/-0) versions.cfg (+1/-1) |
To merge this branch: | bzr merge lp:~wallyworld/launchpad/inline-multicheckbox-widget |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (community) | Approve | ||
Review via email: mp+52943@code.launchpad.net |
Commit message
Add a multicheckbox widget which will PATCH a given attribute on a given resource.
Description of the change
Add a multicheckbox widget which will PATCH a given attribute on a given resource.
== Implementation ==
The widget allows the user to select one of more checkboxes derived from a vocabulary. The vocab is either comes from the field definition of the attribute being edited or can be user specified. When the widget is first rendered, it displays as text the currently selected values. When the user chooses to edit the attribute, a popup displays all available choices with the currently selected items ticked. The user can save/cancel using the tick/cross buttons in the top right of the popup, or click outside the popup to cancel. If the user chooses to save, the widget uses the lp client infrastructure to patch the attribute value on the resource with the new data and re-renders the widget value.
This branch provides the base widget implementation. The lp:~wallyworld/launchpad/inline-recipe-distro-series-edit branch uses the widget to edit the distroseries attribute of a source package recipe.
This branch requires a newer version (rev 206) of lazr-js since it relies on an improvement made to the Activator class.
== Demo and QA ==
A screenshot showing the placement of the edit button:
http://
The edit button is placed to the right of the attribute label.
A screenshot of the widget in action:
http://
== Tests ==
Added new yui tests for the javascript used by the widget:
lp/app/
lp/app/
Add new tests for the widget class setup:
lp/app/
test_
test_
test_
test_
Add new documentation/tests to lazr-js-widgets.txt
== Lint ==
Checking for conflicts and issues in changed files.
Linting changed files:
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/canonical
./lib/lp/
248: E501 line too long (80 characters)
495: E501 line too long (80 characters)
548: E501 line too long (80 characters)
248: Line exceeds 78 characters.
495: Line exceeds 78 characters.
548: Line exceeds 78 characters.
./lib/lp/
1: unbound prefix
Ian Booth (wallyworld) wrote : | # |
> This looks like useful functionality.
>
> Were you aware the lp_save will detect races where the value has changed since
> being read? If so, why did you choose to forgo that protection by doing a raw
> patch?
>
I didn't know that. I based that bit of the code on what was already done in
lp/app/
I'll look at changing over to lp_save as see how that works out.
> How are logged-out users handled? Usually, we show them the edit icon, and
> require them to log in when they click it.
>
Yes, that's what happens here too.
> Did you consider accepting "linkify" as a boolean parameter rather than
> "attribute_type"? Seems simpler, and it's always hard to predict future
> extensions. You might even be able to eliminate "attribute_type" by calling
> canonical_url unconditionally and handling exceptions.
>
I considered it but thought it was too restrictive. I agree it's hard to predict the future, but passing in a "type" value defers the decision to what to do with it to the implementing code. It allows other possible behaviours based on attribute type to be transparently implemented. Having a linkify boolean exposes a specific implementation detail to the caller and precludes future work without requiring an api change.
The second idea - to call canonical_url unconditionally and handle exceptions - I also considered and it was 50/50 which approach I went with. I tend to steer away from using exceptions in this way though. To me they are to communicate errors not probe for functionality etc. I settled for the attribute_type parameter to allow for future flexibility but if you feel its a case of YAGNI, I'll happily use the exception approach. I do think the attribute_type approach is a bit ugly but it was the lesser of 2 evils for me.
> I think you could shorten your code by sticking your json attributes in a
> single dict, and accepting such a dict as the input to addMultiCheckbo
> would be perfectly consistent with YUI3 parameter style.
>
I used the same approach as already used for InlineEditPicke
> Usually, we indent by four spaces, but there are a bunch of places where
> you've done more, including: 94, 133, 194, 413, 514-517, 527, 535-537,
> 539-540, 652-653, 781, 792.
>
Yeah, it's a personal preference - I prefer to try and keep the method parameters close to the method name instead of all the way over to the left. I'll fix them.
> I don't know if we have a style recommendation for how many blank lines to
> spaces to use at the top level, but if we don't, I suggest using the python
> style of two blank lines.
>
Ok.
> According to deryck, who just tested on my behalf, function names are scoped
> to the closure by default, so I think "foo function()" is better than "var foo
> = function()". Is there a reason why you're using th...
Ian Booth (wallyworld) wrote : | # |
I had another look at the javascript infrastructure....
> > Were you aware the lp_save will detect races where the value has changed
> since
> > being read? If so, why did you choose to forgo that protection by doing a
> raw
> > patch?
> >
>
> I didn't know that. I based that bit of the code on what was already done in
> lp/app/
> I'll look at changing over to lp_save as see how that works out.
>
AFAICT, the lp_save is designed to be used in cases where the entire object being edited is modified. It relies on onchange events being used to mark attributes as dirty and calls patch() with the entire set of dirty attributes. Here. we are editing a single attribute - "distroseries" - but there is no distroseries DOM node as such that is called "distroseries", hence the dirty detection doesn't work. The current approach using patch() seems to be the correct one given this use case. And I think the web services infrastructure used to do the patch handles the etag stuff too.
> > How are logged-out users handled? Usually, we show them the edit icon, and
> > require them to log in when they click it.
> >
>
> Yes, that's what happens here too.
>
Actually, what happens for this new widget is the same as for the existing pickers - the edit button is not rendered if the user is not logged in.
Aaron Bentley (abentley) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11-03-16 09:15 PM, Ian Booth wrote:
>> Did you consider accepting "linkify" as a boolean parameter rather than
>> "attribute_type"? Seems simpler, and it's always hard to predict future
>> extensions. You might even be able to eliminate "attribute_type" by calling
>> canonical_url unconditionally and handling exceptions.
>>
>
> I considered it but thought it was too restrictive. I agree it's hard to predict the future, but passing in a "type" value defers the decision to what to do with it to the implementing code. It allows other possible behaviours based on attribute type to be transparently implemented. Having a linkify boolean exposes a specific implementation detail to the caller and precludes future work without requiring an api change.
>
> The second idea - to call canonical_url unconditionally and handle exceptions - I also considered and it was 50/50 which approach I went with. I tend to steer away from using exceptions in this way though. To me they are to communicate errors not probe for functionality etc. I settled for the attribute_type parameter to allow for future flexibility but if you feel its a case of YAGNI, I'll happily use the exception approach. I do think the attribute_type approach is a bit ugly but it was the lesser of 2 evils for me.
I think it's a YAGNI, but I don't insist that you change it.
>> I think you could shorten your code by sticking your json attributes in a
>> single dict, and accepting such a dict as the input to addMultiCheckbo
>> would be perfectly consistent with YUI3 parameter style.
>>
>
> I used the same approach as already used for InlineEditPicke
"config" in YUI3 seems to be a pretty loose term. They tend to use it
the same way python uses keyword arguments.
>> According to deryck, who just tested on my behalf, function names are scoped
>> to the closure by default, so I think "foo function()" is better than "var foo
>> = function()". Is there a reason why you're using the latter?
>>
>
> Depends on what existing code I was grepping to provide guidance on what to do. I tended to follow what had gone before.
I've been following what went before too, but the code I looked at used
named functions, so our existing code is probably inconsistent.
Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk2
m74AnjQv1Vmpkon
=k9pc
-----END PGP SIGNATURE-----
Ian Booth (wallyworld) wrote : | # |
>
> >> I think you could shorten your code by sticking your json attributes in a
> >> single dict, and accepting such a dict as the input to
> addMultiCheckbo
> >> would be perfectly consistent with YUI3 parameter style.
> >>
> >
> > I used the same approach as already used for InlineEditPicke
> with the individual parameters. Note that one of those parameters passed to
> addMultiCheckbo
> InlineEditPicke
> the rational there and consider applying any improvements to both widgets so
> they are implemented consistently.
>
> "config" in YUI3 seems to be a pretty loose term. They tend to use it
> the same way python uses keyword arguments.
>
I did some more digging and I think what's there now is appropriate. The addMultiCheckbo
So I think we should stick with the current implementation given the nature of the code. The other requested changes have been made.
Aaron Bentley (abentley) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11-03-16 10:55 PM, Ian Booth wrote:
> AFAICT, the lp_save is designed to be used in cases where the entire object being edited is modified.
My belief, too.
> It relies on onchange events being used to mark attributes as dirty and calls patch() with the entire set of dirty attributes.
Okay.
> Here. we are editing a single attribute - "distroseries" - but there is no distroseries DOM node as such that is called "distroseries", hence the dirty detection doesn't work.
The web service doesn't deal in DOM nodes AFAIK. It deals in json
structures and Entities, and such.
But I guess you mean that there's no attribute called "distroseries" in
the API, rather a distroseries_
Anyhow, are you saying that lp_save doesn't work with collections at all?
> The current approach using patch() seems to be the correct one given this use case.
> And I think the web services infrastructure used to do the patch
handles the etag stuff too.
The etag stuff was what I was talking about. I can't see anything
handling them, and I don't know how it would acquire information about
the previous etag of the distroseries_
That said, if this is the only way that works, then clearly it's the
right way.
status approved
Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk2
BdYAmQGbcmCbxeC
=gWkA
-----END PGP SIGNATURE-----
Preview Diff
1 | === modified file 'lib/canonical/launchpad/icing/style-3-0.css.in' |
2 | --- lib/canonical/launchpad/icing/style-3-0.css.in 2011-03-08 00:40:50 +0000 |
3 | +++ lib/canonical/launchpad/icing/style-3-0.css.in 2011-03-18 07:43:37 +0000 |
4 | @@ -791,6 +791,13 @@ |
5 | button { |
6 | padding:0; |
7 | } |
8 | +button.overlay-close-button { |
9 | + float: right; |
10 | + width: 15px; |
11 | + height: 15px; |
12 | + display: block; |
13 | + margin-top: 4px; |
14 | + } |
15 | .fieldRequired, .fieldOptional { |
16 | color: #999; |
17 | } |
18 | |
19 | === modified file 'lib/lp/app/browser/lazrjs.py' |
20 | --- lib/lp/app/browser/lazrjs.py 2011-03-10 01:25:13 +0000 |
21 | +++ lib/lp/app/browser/lazrjs.py 2011-03-18 07:43:37 +0000 |
22 | @@ -8,6 +8,7 @@ |
23 | 'BooleanChoiceWidget', |
24 | 'EnumChoiceWidget', |
25 | 'InlineEditPickerWidget', |
26 | + 'InlineMultiCheckboxWidget', |
27 | 'standard_text_html_representation', |
28 | 'TextAreaEditorWidget', |
29 | 'TextLineEditorWidget', |
30 | @@ -19,7 +20,10 @@ |
31 | from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile |
32 | from zope.component import getUtility |
33 | from zope.security.checker import canAccess, canWrite |
34 | -from zope.schema.interfaces import IVocabulary |
35 | +from zope.schema.interfaces import ( |
36 | + ICollection, |
37 | + IVocabulary, |
38 | + ) |
39 | from zope.schema.vocabulary import getVocabularyRegistry |
40 | |
41 | from lazr.enum import IEnumeratedType |
42 | @@ -301,6 +305,114 @@ |
43 | return user and user in vocabulary |
44 | |
45 | |
46 | +class InlineMultiCheckboxWidget(WidgetBase): |
47 | + """Wrapper for the lazr-js multicheckbox widget.""" |
48 | + |
49 | + __call__ = ViewPageTemplateFile( |
50 | + '../templates/inline-multicheckbox-widget.pt') |
51 | + |
52 | + def __init__(self, context, exported_field, |
53 | + label, label_tag="span", attribute_type="default", |
54 | + vocabulary=None, header=None, |
55 | + empty_display_value="None", selected_items=list(), |
56 | + items_tag="span", items_style='', |
57 | + content_box_id=None, edit_view="+edit", edit_url=None, |
58 | + edit_title=''): |
59 | + """Create a widget wrapper. |
60 | + |
61 | + :param context: The object that is being edited. |
62 | + :param exported_field: The attribute being edited. This should be |
63 | + a field from an interface of the form ISomeInterface['fieldname'] |
64 | + :param label: The label text to display above the checkboxes |
65 | + :param label_tag: The tag in which to wrap the label text. |
66 | + :param attribute_type: The attribute type. Currently only "reference" |
67 | + is supported. Used to determine whether to linkify the selected |
68 | + checkbox item values. So ubuntu/hoary becomes |
69 | + http://launchpad.net/devel/api/ubuntu/hoary |
70 | + :param vocabulary: The name of the vocabulary which provides the |
71 | + items or a vocabulary instance. |
72 | + :param header: The text to display as the title of the popup form. |
73 | + :param empty_display_value: The text to display if no items are |
74 | + selected. |
75 | + :param selected_items: The currently selected items. |
76 | + :param items_tag: The tag in which to wrap the items checkboxes. |
77 | + :param items_style: The css style to use for each item checkbox. |
78 | + :param content_box_id: The HTML id to use for this widget. |
79 | + Automatically generated if this is not provided. |
80 | + :param edit_view: The view name to use to generate the edit_url if |
81 | + one is not specified. |
82 | + :param edit_url: The URL to use for editing when the user isn't logged |
83 | + in and when JS is off. Defaults to the edit_view on the context. |
84 | + :param edit_title: Used to set the title attribute of the anchor. |
85 | + |
86 | + """ |
87 | + super(InlineMultiCheckboxWidget, self).__init__( |
88 | + context, exported_field, content_box_id, |
89 | + edit_view, edit_url, edit_title) |
90 | + |
91 | + linkify_items = attribute_type == "reference" |
92 | + |
93 | + if header is None: |
94 | + header = self.exported_field.title+":" |
95 | + self.header = header, |
96 | + self.empty_display_value = empty_display_value |
97 | + self.label = label |
98 | + self.label_open_tag = "<%s>" % label_tag |
99 | + self.label_close_tag = "</%s>" % label_tag |
100 | + self.items = selected_items |
101 | + self.items_open_tag = ("<%s id='%s'>" % |
102 | + (items_tag, self.content_box_id+"-items")) |
103 | + self.items_close_tag = "</%s>" % items_tag |
104 | + self.linkify_items = linkify_items |
105 | + |
106 | + if vocabulary is None: |
107 | + if ICollection.providedBy(exported_field): |
108 | + vocabulary = exported_field.value_type.vocabularyName |
109 | + else: |
110 | + vocabulary = exported_field.vocabularyName |
111 | + |
112 | + |
113 | + if isinstance(vocabulary, basestring): |
114 | + vocabulary = getVocabularyRegistry().get(context, vocabulary) |
115 | + |
116 | + # Construct checkbox data dict for each item in the vocabulary. |
117 | + items = [] |
118 | + style = ';'.join(['font-weight: normal', items_style]) |
119 | + for item in vocabulary: |
120 | + item_value = item.value if safe_hasattr(item, 'value') else item |
121 | + checked = item_value in selected_items |
122 | + if linkify_items: |
123 | + save_value = canonical_url(item_value, force_local_path=True) |
124 | + else: |
125 | + save_value = item_value.name |
126 | + new_item = { |
127 | + 'name': item.title, |
128 | + 'token': item.token, |
129 | + 'style': style, |
130 | + 'checked': checked, |
131 | + 'value': save_value} |
132 | + items.append(new_item) |
133 | + self.has_choices = len(items) |
134 | + |
135 | + # JSON encoded attributes. |
136 | + self.json_content_box_id = simplejson.dumps(self.content_box_id) |
137 | + self.json_attribute = simplejson.dumps(self.api_attribute) |
138 | + self.json_attribute_type = simplejson.dumps(attribute_type) |
139 | + self.json_items = simplejson.dumps(items) |
140 | + self.json_description = simplejson.dumps( |
141 | + self.exported_field.description) |
142 | + |
143 | + @property |
144 | + def config(self): |
145 | + return dict( |
146 | + header=self.header, |
147 | + ) |
148 | + |
149 | + @property |
150 | + def json_config(self): |
151 | + return simplejson.dumps(self.config) |
152 | + |
153 | + |
154 | def vocabulary_to_choice_edit_items( |
155 | vocab, css_class_prefix=None, disabled_items=None, as_json=False, |
156 | name_fn=None, value_fn=None): |
157 | |
158 | === added file 'lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py' |
159 | --- lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py 1970-01-01 00:00:00 +0000 |
160 | +++ lib/lp/app/browser/tests/test_inlinemulticheckboxwidget.py 2011-03-18 07:43:37 +0000 |
161 | @@ -0,0 +1,82 @@ |
162 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
163 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
164 | + |
165 | +"""Tests for the InlineMultiCheckboxWidget.""" |
166 | + |
167 | +__metaclass__ = type |
168 | + |
169 | +import simplejson |
170 | + |
171 | +from zope.interface import Interface |
172 | +from zope.schema import List |
173 | +from zope.schema._field import Choice |
174 | +from zope.schema.vocabulary import getVocabularyRegistry |
175 | + |
176 | +from lazr.enum import EnumeratedType, Item |
177 | + |
178 | +from canonical.launchpad.webapp.publisher import canonical_url |
179 | +from canonical.testing.layers import DatabaseFunctionalLayer |
180 | +from lp.app.browser.lazrjs import InlineMultiCheckboxWidget |
181 | +from lp.testing import ( |
182 | + TestCaseWithFactory, |
183 | + ) |
184 | + |
185 | + |
186 | +class Alphabet(EnumeratedType): |
187 | + """A vocabulary for testing.""" |
188 | + A = Item("A", "Letter A") |
189 | + B = Item("B", "Letter B") |
190 | + |
191 | + |
192 | +class TestInlineMultiCheckboxWidget(TestCaseWithFactory): |
193 | + |
194 | + layer = DatabaseFunctionalLayer |
195 | + |
196 | + def _getWidget(self, **kwargs): |
197 | + |
198 | + class ITest(Interface): |
199 | + test_field = List( |
200 | + Choice(vocabulary='BuildableDistroSeries')) |
201 | + return InlineMultiCheckboxWidget( |
202 | + None, ITest['test_field'], "Label", edit_url='fake', **kwargs) |
203 | + |
204 | + def _makeExpectedItems(self, vocab, selected=list(), value_fn=None): |
205 | + if value_fn is None: |
206 | + value_fn = lambda item: item.value.name |
207 | + expected_items = [] |
208 | + style = 'font-weight: normal;' |
209 | + for item in vocab: |
210 | + new_item = { |
211 | + 'name': item.title, |
212 | + 'token': item.token, |
213 | + 'style': style, |
214 | + 'checked': (item.value in selected), |
215 | + 'value': value_fn(item)} |
216 | + expected_items.append(new_item) |
217 | + return expected_items |
218 | + |
219 | + def test_items_for_field_vocabulary(self): |
220 | + widget = self._getWidget(attribute_type="reference") |
221 | + vocab = getVocabularyRegistry().get(None, 'BuildableDistroSeries') |
222 | + value_fn = lambda item: canonical_url( |
223 | + item.value, force_local_path=True) |
224 | + expected_items = self._makeExpectedItems(vocab, value_fn=value_fn) |
225 | + self.assertEqual(simplejson.dumps(expected_items), widget.json_items) |
226 | + |
227 | + def test_items_for_custom_vocabulary(self): |
228 | + widget = self._getWidget(vocabulary=Alphabet) |
229 | + expected_items = self._makeExpectedItems(Alphabet) |
230 | + self.assertEqual(simplejson.dumps(expected_items), widget.json_items) |
231 | + |
232 | + def test_items_for_custom_vocabulary_name(self): |
233 | + widget = self._getWidget(vocabulary="CountryName") |
234 | + vocab = getVocabularyRegistry().get(None, "CountryName") |
235 | + expected_items = self._makeExpectedItems(vocab) |
236 | + self.assertEqual(simplejson.dumps(expected_items), widget.json_items) |
237 | + |
238 | + def test_selected_items_checked(self): |
239 | + widget = self._getWidget( |
240 | + vocabulary=Alphabet, selected_items=[Alphabet.A]) |
241 | + expected_items = self._makeExpectedItems( |
242 | + Alphabet, selected=[Alphabet.A]) |
243 | + self.assertEqual(simplejson.dumps(expected_items), widget.json_items) |
244 | |
245 | === modified file 'lib/lp/app/doc/lazr-js-widgets.txt' |
246 | --- lib/lp/app/doc/lazr-js-widgets.txt 2011-03-10 00:40:15 +0000 |
247 | +++ lib/lp/app/doc/lazr-js-widgets.txt 2011-03-18 07:43:37 +0000 |
248 | @@ -399,3 +399,95 @@ |
249 | |
250 | The edit link can be changed in exactly the same way as for the |
251 | TextLineEditorWidget above. |
252 | + |
253 | + |
254 | +InlineMultiCheckboxWidget |
255 | +------------------- |
256 | + |
257 | +This widget is used to edit fields which are Lists or Sets. It displays the |
258 | +current items in the collection when the page is rendered and provides the |
259 | +ability to edit the selected items via a popup overlay. The popup has a set of |
260 | +checkboxes for selecting one or more items from a vocabulary. The vocabulary |
261 | +defaults to that associated with the field being edited but can be user |
262 | +defined. |
263 | + |
264 | + >>> from lp.app.browser.lazrjs import InlineMultiCheckboxWidget |
265 | + |
266 | +The bare minimum that you need to provide the widget is the object that you |
267 | +are editing, and the exported field that is being edited, and the label to |
268 | +display for the set of checkboxes. |
269 | + |
270 | +The surrounding tag for the label and set of checkboxes are both customisable, |
271 | +and a prefix may be given. The prefix is rendered as part of the widget, but |
272 | +isn't updated when the value changes. |
273 | + |
274 | +Other customisable parameters include the popup header text (defaults to the |
275 | +field title suffixed by ":"), the string to render when the field contains no |
276 | +selected items (defaults to "None"), and a CSS style to add to each checkbox |
277 | +node (defaults to ''). |
278 | + |
279 | +If the user does not have edit rights, the widget just renders the text based |
280 | +on the current value of the field on the object: |
281 | + |
282 | + >>> login(ANONYMOUS) |
283 | + >>> from lp.code.interfaces.sourcepackagerecipe import ( |
284 | + ... ISourcePackageRecipe, |
285 | + ... ) |
286 | + >>> distroseries = ISourcePackageRecipe['distroseries'] |
287 | + >>> recipe = factory.makeSourcePackageRecipe( |
288 | + ... owner=eric, name=u'cake_recipe', description=u'Yummy.') |
289 | + >>> widget = InlineMultiCheckboxWidget( |
290 | + ... recipe, distroseries, 'Recipe distro series', |
291 | + ... header='Select distroseries:', vocabulary='BuildableDistroSeries', |
292 | + ... label_tag='dt', items_tag='dl', |
293 | + ... selected_items=recipe.distroseries) |
294 | + >>> print widget() |
295 | + <span id="edit-distroseries"> |
296 | + <dt> |
297 | + Recipe distro series |
298 | + </dt> |
299 | + <span class="yui3-activator-data-box"> |
300 | + <dl id='edit-distroseries-items'> |
301 | + ... |
302 | + </span> |
303 | + <div class="yui3-activator-message-box yui3-activator-hidden" /> |
304 | + </span> |
305 | + |
306 | +If the user has edit rights, an edit icon is rendered and some javascript is |
307 | +rendered to hook up the widget. |
308 | + |
309 | + >>> login_person(eric) |
310 | + >>> print widget() |
311 | + <span id="edit-distroseries"> |
312 | + <BLANKLINE> |
313 | + <dt> |
314 | + Recipe distro series |
315 | + <BLANKLINE> |
316 | + <button class="lazr-btn yui3-activator-act yui3-activator-hidden" |
317 | + id="edit-distroseries-btn"> |
318 | + Edit |
319 | + </button> |
320 | + <BLANKLINE> |
321 | + <noscript> |
322 | + <a class="sprite edit" |
323 | + href="http://code.launchpad.dev/~eric/+recipe/cake_recipe/+edit" |
324 | + title=""></a> |
325 | + </noscript> |
326 | + </dt> |
327 | + <BLANKLINE> |
328 | + <span class="yui3-activator-data-box"> |
329 | + <dl id='edit-distroseries-items'> |
330 | + ... |
331 | + <div class="yui3-activator-message-box yui3-activator-hidden" /> |
332 | + </span> |
333 | + <script> |
334 | + LPS.use('lp.app.multicheckbox', function(Y) { |
335 | + ... |
336 | + </script> |
337 | + |
338 | + |
339 | +Changing the edit link |
340 | +********************** |
341 | + |
342 | +The edit link can be changed in exactly the same way as for the |
343 | +TextLineEditorWidget above. |
344 | |
345 | === added file 'lib/lp/app/javascript/multicheckbox.js' |
346 | --- lib/lp/app/javascript/multicheckbox.js 1970-01-01 00:00:00 +0000 |
347 | +++ lib/lp/app/javascript/multicheckbox.js 2011-03-18 07:43:37 +0000 |
348 | @@ -0,0 +1,232 @@ |
349 | +YUI.add('lp.app.multicheckbox', function(Y) { |
350 | + |
351 | +var namespace = Y.namespace('lp.app.multicheckbox'); |
352 | + |
353 | +/* Add a multicheckbox widget which will PATCH a given attribute on |
354 | + * a given resource. |
355 | + * |
356 | + * @method addMultiCheckboxPatcher |
357 | + * @param {Array} items The items which to display as checkboxes. |
358 | + * @param {String} help_text The text to display beneath the checkboxes. |
359 | + * @param {String} resource_uri The object being modified. |
360 | + * @param {String} attribute_name The attribute on the resource being |
361 | + * modified. |
362 | + * @param {String} attribute_type The attribute type. |
363 | + * "reference": the items are object references |
364 | + * Other values are currently ignored. |
365 | + * @param {String} content_box_id |
366 | + * @param {Object} config Object literal of config name/value pairs. |
367 | + * config.header: a line of text at the top of the widget. |
368 | + * config.step_title: overrides the subtitle. |
369 | + */ |
370 | +namespace.addMultiCheckboxPatcher = function ( |
371 | + items, help_text, resource_uri, attribute_name, attribute_type, |
372 | + content_box_id, config, client) { |
373 | + |
374 | + |
375 | + if (Y.UA.ie) { |
376 | + return; |
377 | + } |
378 | + |
379 | + // We may have been passed a mock client for testing but if not, create |
380 | + // a proper one. |
381 | + if (client === undefined) |
382 | + client = new Y.lp.client.Launchpad(); |
383 | + |
384 | + var content_box = Y.one('#'+content_box_id); |
385 | + var result_node = Y.one('#'+content_box_id+'-items'); |
386 | + var widget_node = Y.one('#'+attribute_name); |
387 | + var activator = new Y.lazr.activator.Activator( |
388 | + {contentBox: content_box, animationNode: widget_node}); |
389 | + |
390 | + var failure_handler = function (id, response, args) { |
391 | + activator.renderFailure( |
392 | + Y.Node.create( |
393 | + '<div>' + response.statusText + |
394 | + '<pre>' + response.responseText + '</pre>' + |
395 | + '</div>')); |
396 | + }; |
397 | + |
398 | + // The function called to save the selected items. |
399 | + function save(editform, item_value_mapping) { |
400 | + var choice_nodes = Y.one('[id="'+attribute_name+'.items"]'); |
401 | + var result = namespace.getSelectedItems( |
402 | + choice_nodes, item_value_mapping, attribute_type); |
403 | + activator.renderProcessing(); |
404 | + var success_handler = function (entry) { |
405 | + var xhtml = entry.getHTML(attribute_name); |
406 | + result_node.set('innerHTML', xhtml); |
407 | + activator.renderSuccess(result_node); |
408 | + }; |
409 | + |
410 | + var patch_payload = {}; |
411 | + patch_payload[attribute_name] = result; |
412 | + client.patch(editform._resource_uri, patch_payload, { |
413 | + accept: 'application/json;include=lp_html', |
414 | + on: { |
415 | + success: success_handler, |
416 | + failure: failure_handler |
417 | + } |
418 | + }); |
419 | + } |
420 | + |
421 | + config.save = save; |
422 | + config.content_box_id = content_box_id; |
423 | + var editform = namespace.create(attribute_name, items, help_text, config); |
424 | + editform._resource_uri = resource_uri; |
425 | + var extra_buttons = Y.Node.create( |
426 | + '<div style="text-align: center; height: 3em; ' + |
427 | + 'white-space: nowrap"/>'); |
428 | + activator.subscribe('act', function (e) { |
429 | + editform.show(); |
430 | + }); |
431 | + activator.render(); |
432 | + return editform; |
433 | +}; |
434 | + |
435 | + |
436 | +/** |
437 | + * Creates a multicheckbox widget that has already been rendered and hidden. |
438 | + * |
439 | + * @requires dom, lazr.activator, lazr.overlay |
440 | + * @method create |
441 | + * @param {String} attribute_name The attribute on the resource being |
442 | + * modified. |
443 | + * @param {Array} items Items for which to create checkbox elements. |
444 | + * @param {String} help_text text display below the checkboxes. |
445 | + * @param {Object} config Optional Object literal of config name/value pairs. |
446 | + * config.header is a line of text at the top of |
447 | + * the widget. |
448 | + * config.save is a Function (optional) which takes |
449 | + * a single string argument. |
450 | + */ |
451 | +namespace.create = function (attribute_name, items, help_text, config) { |
452 | + if (Y.UA.ie) { |
453 | + return; |
454 | + } |
455 | + |
456 | + if (config !== undefined) { |
457 | + var header = 'Choose an item.'; |
458 | + if (config.header !== undefined) { |
459 | + header = config.header; |
460 | + } |
461 | + } |
462 | + |
463 | + var new_config = Y.merge(config, { |
464 | + align: { |
465 | + points: [Y.WidgetPositionAlign.CC, |
466 | + Y.WidgetPositionAlign.CC] |
467 | + }, |
468 | + progressbar: true, |
469 | + progress: 100, |
470 | + headerContent: "<h2>" + header + "</h2>", |
471 | + centered: true, |
472 | + zIndex: 1000, |
473 | + visible: false |
474 | + }); |
475 | + |
476 | + // We use a pretty overlay to display the checkboxes. |
477 | + var editform = new Y.lazr.PrettyOverlay(new_config); |
478 | + |
479 | + // The html for each checkbox. |
480 | + var CHECKBOX_TEMPLATE = |
481 | + ['<label style="{item_style}" for="{field_name}.{field_index}">', |
482 | + '<input id="{field_name}.{field_index}" ', |
483 | + 'name="{field_name}.{field_index}" ', |
484 | + 'class="checkboxType" type="checkbox" value="{field_value}" ', |
485 | + '{item_checked}> {field_text}</label>'].join(""); |
486 | + |
487 | + var content = Y.Node.create("<div/>"); |
488 | + var header_node = Y.Node.create( |
489 | + "<div class='yui3-lazr-formoverlay-form-header'/>"); |
490 | + content.appendChild(header_node); |
491 | + var body = Y.Node.create("<div class='yui3-widget-bd'/>"); |
492 | + |
493 | + // Set up the nodes for each checkbox. |
494 | + var choices_nodes = Y.Node.create('<ul id="'+attribute_name+'.items"/>'); |
495 | + // A mapping from checkbox value attributes (data token) -> data values |
496 | + var item_value_mapping = {}; |
497 | + Y.Array.each(items, function(data, i) { |
498 | + var checked_html = ''; |
499 | + if (data.checked) |
500 | + checked_html = 'checked="checked"'; |
501 | + var checkbox_html = Y.Lang.substitute( |
502 | + CHECKBOX_TEMPLATE, |
503 | + {field_name: "field."+attribute_name, field_index:i, |
504 | + field_value: data.token, field_text: data.name, |
505 | + item_style: data.style, item_checked: checked_html}); |
506 | + |
507 | + var choice_item = Y.Node.create("<li/>"); |
508 | + choice_item.set("innerHTML", checkbox_html); |
509 | + choices_nodes.appendChild(choice_item); |
510 | + item_value_mapping[data.token] = data.value; |
511 | + }, this); |
512 | + body.appendChild(choices_nodes); |
513 | + content.appendChild(body); |
514 | + var help_node = Y.Node.create("<p class='formHelp'>"+help_text+"</p>"); |
515 | + content.appendChild(help_node); |
516 | + editform.set('bodyContent', content); |
517 | + |
518 | + // We replace the default Close button (x) with our own save/cancel ones. |
519 | + var close_node = editform.get('boundingBox').one('div.close'); |
520 | + var orig_close_button = close_node.one('a'); |
521 | + orig_close_button.setAttribute('style', 'display: none'); |
522 | + var save_button = Y.Node.create( |
523 | + '<button id="'+config.content_box_id+'-save" ' + |
524 | + 'class="overlay-close-button lazr-pos lazr-btn">Ok</button>'); |
525 | + var close_button = Y.Node.create( |
526 | + '<button class="overlay-close-button lazr-neg lazr-btn">Cancel' + |
527 | + '</button>'); |
528 | + save_button.on('click', function(e) { |
529 | + e.halt(); |
530 | + editform.hide(); |
531 | + config.save(editform, item_value_mapping); |
532 | + }); |
533 | + close_button.on('click', function(e) { |
534 | + e.halt(); |
535 | + editform.fire('cancel'); |
536 | + }); |
537 | + close_node.appendChild(close_button); |
538 | + close_node.appendChild(save_button); |
539 | + editform.render(); |
540 | + editform.hide(); |
541 | + return editform; |
542 | +}; |
543 | + |
544 | + |
545 | +/* |
546 | + * Return a list of the selected checkbox values. |
547 | + * Exposed via the namespace so it is accessible to tests. |
548 | + */ |
549 | +namespace.getSelectedItems = function(choice_nodes, item_value_mapping, |
550 | + attribute_type) { |
551 | + var result = []; |
552 | + choice_nodes.all('.checkboxType').each(function(item) { |
553 | + if (item.get("checked")) { |
554 | + var item_token = item.getAttribute("value"); |
555 | + var item_value = item_value_mapping[item_token]; |
556 | + var marshalled_value = marshall(item_value, attribute_type); |
557 | + result.push(marshalled_value); |
558 | + } |
559 | + }); |
560 | + return result; |
561 | +}; |
562 | + |
563 | + |
564 | +/* |
565 | + * Transform the selected value according to the attribute type we are editing |
566 | + */ |
567 | +function marshall(value, attribute_type) { |
568 | + switch (attribute_type) { |
569 | + case "reference": |
570 | + var item_value = Y.lp.client.normalize_uri(value); |
571 | + return Y.lp.client.get_absolute_uri(item_value); |
572 | + break; |
573 | + default: |
574 | + return value; |
575 | + } |
576 | +} |
577 | + |
578 | +}, "0.1", {"requires": [ |
579 | + "dom", "lazr.overlay", "lazr.activator", "lp.client" |
580 | + ]}); |
581 | |
582 | === added file 'lib/lp/app/javascript/tests/test_multicheckboxwidget.html' |
583 | --- lib/lp/app/javascript/tests/test_multicheckboxwidget.html 1970-01-01 00:00:00 +0000 |
584 | +++ lib/lp/app/javascript/tests/test_multicheckboxwidget.html 2011-03-18 07:43:37 +0000 |
585 | @@ -0,0 +1,42 @@ |
586 | +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" |
587 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
588 | +<html> |
589 | + <head> |
590 | + <title>Multicheckbox Widget</title> |
591 | + |
592 | + <!-- YUI 3.0 Setup --> |
593 | + <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script> |
594 | + <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/> |
595 | + <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/> |
596 | + <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/> |
597 | + <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" /> |
598 | + |
599 | + <!-- Some required dependencies --> |
600 | + <script type="text/javascript" src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script> |
601 | + <script type="text/javascript" src="../client.js"></script> |
602 | + |
603 | + <!-- The module under test --> |
604 | + <script type="text/javascript" src="../multicheckbox.js"></script> |
605 | + |
606 | + <!-- The test suite --> |
607 | + <script type="text/javascript" src="test_multicheckboxwidget.js"></script> |
608 | +</head> |
609 | +<body class="yui3-skin-sam"> |
610 | + <span id="edit-test"> |
611 | + <span id="multicheckboxtest"> |
612 | + <span> |
613 | + A multicheckbox widget test |
614 | + <button id="edit-multicheckboxtest-btn" |
615 | + class="lazr-btn yui3-activator-act yui3-activator-hidden"> |
616 | + Edit |
617 | + </button> |
618 | + </span> |
619 | + <span class="yui3-activator-data-box"> |
620 | + <span id="edit-test-items"/> |
621 | + </span> |
622 | + <div class="yui3-activator-message-box yui3-activator-hidden"/> |
623 | + </span> |
624 | + </span> |
625 | + <div id="log"></div> |
626 | +</body> |
627 | +</html> |
628 | \ No newline at end of file |
629 | |
630 | === added file 'lib/lp/app/javascript/tests/test_multicheckboxwidget.js' |
631 | --- lib/lp/app/javascript/tests/test_multicheckboxwidget.js 1970-01-01 00:00:00 +0000 |
632 | +++ lib/lp/app/javascript/tests/test_multicheckboxwidget.js 2011-03-18 07:43:37 +0000 |
633 | @@ -0,0 +1,184 @@ |
634 | +/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */ |
635 | + |
636 | +YUI({ |
637 | + base: '../../../../canonical/launchpad/icing/yui/', |
638 | + filter: 'raw', |
639 | + combine: false, |
640 | + fetchCSS: false |
641 | + }).use('test', 'console', 'dom', 'event', 'event-simulate', |
642 | + 'lazr.overlay', 'lazr.activator', |
643 | + 'lp.client', 'lp.app.multicheckbox', |
644 | + function(Y) { |
645 | + |
646 | +var suite = new Y.Test.Suite("lp.app.multicheckbox Tests"); |
647 | + |
648 | +/* |
649 | + * A wrapper for the Y.Event.simulate() function. The wrapper accepts |
650 | + * CSS selectors and Node instances instead of raw nodes. |
651 | + */ |
652 | +function simulate(widget, selector, evtype, options) { |
653 | + var rawnode = Y.Node.getDOMNode(widget.one(selector)); |
654 | + Y.Event.simulate(rawnode, evtype, options); |
655 | +} |
656 | + |
657 | +/* Helper function to clean up a dynamically added widget instance. */ |
658 | +function cleanup_widget(widget) { |
659 | + // Nuke the boundingBox, but only if we've touched the DOM. |
660 | + if (widget.get('rendered')) { |
661 | + var bb = widget.get('boundingBox'); |
662 | + bb.get('parentNode').removeChild(bb); |
663 | + } |
664 | + // Kill the widget itself. |
665 | + widget.destroy(); |
666 | +} |
667 | + |
668 | +var MockClient = function() { |
669 | + /* A mock to provide the result of a patch operation. */ |
670 | +}; |
671 | + |
672 | +MockClient.prototype = { |
673 | + 'patch': function(uri, representation, config, headers) { |
674 | + var patch_content = new Y.lp.client.Entry(); |
675 | + var html = representation['multicheckboxtest']; |
676 | + patch_content.set('lp_html', {'multicheckboxtest': html}); |
677 | + config.on.success(patch_content); |
678 | + } |
679 | +}; |
680 | + |
681 | +suite.add(new Y.Test.Case({ |
682 | + name: "lp.app.multicheckbox", |
683 | + |
684 | + createWidget: function(header) { |
685 | + // Create a widget with some default data |
686 | + var config = {"empty_display_value": "None"}; |
687 | + if( header != 'None' ) { |
688 | + if( header == null ) |
689 | + header = 'Test multicheckbox widget:'; |
690 | + config['header'] = header; |
691 | + } |
692 | + |
693 | + var mock_client = new MockClient(); |
694 | + this.widget = Y.lp.app.multicheckbox.addMultiCheckboxPatcher( |
695 | + [{"token": "0", "style": "font-weight: normal;", "checked": true, |
696 | + "name": "Item 0", "value": "item1"}, |
697 | + {"token": "1", "style": "font-weight: normal;", "checked": false, |
698 | + "name": "Item 1", "value": "item2"}], |
699 | + 'A test', |
700 | + '/~fred/+recipe/a-recipe', |
701 | + 'multicheckboxtest', |
702 | + 'reference', |
703 | + 'edit-test', |
704 | + config, mock_client); |
705 | + }, |
706 | + |
707 | + tearDown: function() { |
708 | + cleanup_widget(this.widget); |
709 | + }, |
710 | + |
711 | + test_widget_can_be_instantiated: function() { |
712 | + this.createWidget(); |
713 | + Y.Assert.isInstanceOf( |
714 | + Y.lazr.PrettyOverlay, this.widget, |
715 | + "Widget failed to be instantiated"); |
716 | + }, |
717 | + |
718 | + test_header_value: function() { |
719 | + // Check the header text value. |
720 | + this.createWidget(); |
721 | + var header = Y.one('.yui3-widget-hd'); |
722 | + Y.Assert.areEqual( |
723 | + header.get('innerHTML'), '<h2>Test multicheckbox widget:</h2>'); |
724 | + }, |
725 | + |
726 | + test_default_header_value: function() { |
727 | + // Check the default header text value. |
728 | + this.createWidget(header='None'); |
729 | + var header = Y.one('.yui3-widget-hd'); |
730 | + Y.Assert.areEqual( |
731 | + header.get('innerHTML'), '<h2>Choose an item.</h2>'); |
732 | + }, |
733 | + |
734 | + test_help_text_value: function() { |
735 | + // Check the help text value. |
736 | + this.createWidget(); |
737 | + var header = Y.one('.yui3-widget-bd p.formHelp'); |
738 | + Y.Assert.areEqual( |
739 | + header.get('innerHTML'), 'A test'); |
740 | + }, |
741 | + |
742 | + test_widget_has_correct_choice_data: function() { |
743 | + // Make sure the checkboxes are rendered with expected data values.. |
744 | + this.createWidget(); |
745 | + for( var x=0; x<2; x++ ) { |
746 | + var item = Y.one( |
747 | + 'input[id="field.multicheckboxtest.'+x+'][type=checkbox]'); |
748 | + Y.Assert.areEqual(item.getAttribute('value'), x); |
749 | + Y.Assert.areEqual(item.get('checked'), x==0); |
750 | + } |
751 | + }, |
752 | + |
753 | + test_widget_has_correct_choice_text: function() { |
754 | + // Make sure the checkboxes are rendered with expected label text. |
755 | + this.createWidget(); |
756 | + for( var x=0; x<2; x++ ) { |
757 | + var item = Y.one('label[for="field.multicheckboxtest.'+x+']'); |
758 | + var txt = item.get('textContent'); |
759 | + //remove any   in the text |
760 | + txt = txt.replace(/^[\s\xA0]+/g,'').replace(/[\s( )]+$/g,''); |
761 | + Y.Assert.areEqual(txt, 'Item '+ x); |
762 | + } |
763 | + }, |
764 | + |
765 | + test_getSelectedItems: function() { |
766 | + // Test that getSelectedItems returns the correct values. |
767 | + this.createWidget(); |
768 | + var items = Y.one('[id="multicheckboxtest.items"]'); |
769 | + var mapping = {0: 'Item0', 1: 'Item1'}; |
770 | + var selected = Y.lp.app.multicheckbox.getSelectedItems( |
771 | + items, mapping, ''); |
772 | + Y.ArrayAssert.itemsAreEqual(['Item0'], selected); |
773 | + }, |
774 | + |
775 | + test_marshall_references: function() { |
776 | + // Test that getSelectedItems returns the correct values for reference |
777 | + // values which are links to domain objects. |
778 | + this.createWidget(); |
779 | + var items = Y.one('[id="multicheckboxtest.items"]'); |
780 | + var mapping = {0: '/ubuntu/Item0', 1: '/ubuntu/Item1'}; |
781 | + var selected = Y.lp.app.multicheckbox.getSelectedItems( |
782 | + items, mapping, 'reference'); |
783 | + var item_value = Y.lp.client.normalize_uri(selected[0]); |
784 | + var link = Y.lp.client.get_absolute_uri(item_value); |
785 | + Y.ArrayAssert.itemsAreEqual([link], selected); |
786 | + }, |
787 | + |
788 | + test_widget_content_from_patch_success: function() { |
789 | + // Test that when the user clicks "save" and the result comes back, |
790 | + // the DOM is correctly updated. |
791 | + this.createWidget(); |
792 | + simulate(this.widget.get('boundingBox'), '#edit-test-save', 'click'); |
793 | + var test_content = 'file:///api/devel/item1'; |
794 | + var items = Y.one('[id="edit-test-items"]'); |
795 | + Y.Assert.areEqual(test_content, items.get('innerHTML')); |
796 | + } |
797 | + |
798 | +})); |
799 | + |
800 | +// Lock, stock, and two smoking barrels. |
801 | +var handle_complete = function(data) { |
802 | + status_node = Y.Node.create( |
803 | + '<p id="complete">Test status: complete</p>'); |
804 | + Y.one('body').appendChild(status_node); |
805 | + }; |
806 | +Y.Test.Runner.on('complete', handle_complete); |
807 | +Y.Test.Runner.add(suite); |
808 | + |
809 | +var yui_console = new Y.Console({ |
810 | + newestOnTop: false |
811 | +}); |
812 | +yui_console.render('#log'); |
813 | + |
814 | +Y.on('domready', function() { |
815 | + Y.Test.Runner.run(); |
816 | +}); |
817 | +}); |
818 | |
819 | === added file 'lib/lp/app/templates/inline-multicheckbox-widget.pt' |
820 | --- lib/lp/app/templates/inline-multicheckbox-widget.pt 1970-01-01 00:00:00 +0000 |
821 | +++ lib/lp/app/templates/inline-multicheckbox-widget.pt 2011-03-18 07:43:37 +0000 |
822 | @@ -0,0 +1,50 @@ |
823 | +<span tal:define="items view/items|nothing" |
824 | + tal:attributes="id view/content_box_id"> |
825 | + <tal:label-open-tag replace="structure view/label_open_tag"/> |
826 | + <span tal:replace="structure view/label"/> |
827 | + <tal:has_choices condition="python:view.has_choices and view.can_write"> |
828 | + <button tal:attributes="id string:${view/content_box_id}-btn" |
829 | + class="lazr-btn yui3-activator-act yui3-activator-hidden"> |
830 | + Edit |
831 | + </button> |
832 | + </tal:has_choices> |
833 | + <noscript tal:condition="view/can_write"> |
834 | + <a tal:attributes="href view/edit_url; |
835 | + title view/edit_title" |
836 | + class="sprite edit"></a> |
837 | + </noscript> |
838 | + <tal:label-close-tag replace="structure view/label_close_tag"/> |
839 | + <span class="yui3-activator-data-box"> |
840 | + <tal:items-open-tag replace="structure view/items_open_tag"/> |
841 | + <span tal:condition="not:items" tal:content="structure view/empty_display_value"/> |
842 | + <ul tal:condition="items"> |
843 | + <li tal:condition="view/linkify_items" |
844 | + tal:repeat="item items" |
845 | + tal:content="structure item/fmt:link"/> |
846 | + <li tal:condition="not:view/linkify_items" |
847 | + tal:repeat="item items" |
848 | + tal:content="structure item"/> |
849 | + </ul> |
850 | + <tal:items-close-tag replace="structure view/items_close_tag"/> |
851 | + </span> |
852 | + <div class="yui3-activator-message-box yui3-activator-hidden"/> |
853 | +</span> |
854 | +<script tal:condition="view/can_write" |
855 | + tal:content="structure string: |
856 | +LPS.use('lp.app.multicheckbox', function(Y) { |
857 | + if (Y.UA.ie) { |
858 | + return; |
859 | + } |
860 | + |
861 | + Y.on('load', function(e) { |
862 | + Y.lp.app.multicheckbox.addMultiCheckboxPatcher( |
863 | + ${view/json_items}, |
864 | + ${view/json_description}, |
865 | + ${view/json_resource_uri}, |
866 | + ${view/json_attribute}, |
867 | + ${view/json_attribute_type}, |
868 | + ${view/json_content_box_id}, |
869 | + ${view/json_config}); |
870 | + }, window); |
871 | +}); |
872 | +"/> |
873 | |
874 | === modified file 'versions.cfg' |
875 | --- versions.cfg 2011-03-15 21:09:44 +0000 |
876 | +++ versions.cfg 2011-03-18 07:43:37 +0000 |
877 | @@ -39,7 +39,7 @@ |
878 | lazr.smtptest = 1.1 |
879 | lazr.testing = 0.1.1 |
880 | lazr.uri = 1.0.2 |
881 | -lazr-js = 1.6DEV-r202 |
882 | +lazr-js = 1.6DEV-r206 |
883 | manuel = 1.1.1 |
884 | martian = 0.11 |
885 | mechanize = 0.1.11 |
This looks like useful functionality.
Were you aware the lp_save will detect races where the value has changed since being read? If so, why did you choose to forgo that protection by doing a raw patch?
How are logged-out users handled? Usually, we show them the edit icon, and require them to log in when they click it.
Did you consider accepting "linkify" as a boolean parameter rather than "attribute_type"? Seems simpler, and it's always hard to predict future extensions. You might even be able to eliminate "attribute_type" by calling canonical_url unconditionally and handling exceptions.
I think you could shorten your code by sticking your json attributes in a single dict, and accepting such a dict as the input to addMultiCheckbo xPatcher would be perfectly consistent with YUI3 parameter style.
Usually, we indent by four spaces, but there are a bunch of places where you've done more, including: 94, 133, 194, 413, 514-517, 527, 535-537, 539-540, 652-653, 781, 792.
I don't know if we have a style recommendation for how many blank lines to spaces to use at the top level, but if we don't, I suggest using the python style of two blank lines.
According to deryck, who just tested on my behalf, function names are scoped to the closure by default, so I think "foo function()" is better than "var foo = function()". Is there a reason why you're using the latter?
You seem a bit inconsistent about spaces when using "+" for string concatenation. I suggest using a space, e.g. "foo" + "bar", not "foo"+"bar". This is more like python style, and I find it more readable.
On a related note, CHECKBOX_TEMPLATE should use the [str1, str2, ...].join("") form per https:/ /dev.launchpad. net/JavaScriptR eviewNotes
The expected_item generation in test_items_ for_field_ vocabulary, test_items_ for_custom_ vocabulary and test_items_ for_custom_ vocabulary_ name looks duplicated, and should be extracted to a helper.