Merge lp:~methanal-developers/methanal/enum-object-and-group-choices into lp:methanal
- enum-object-and-group-choices
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Tristan Seligmann | ||||||||
Approved revision: | 151 | ||||||||
Merged at revision: | 143 | ||||||||
Proposed branch: | lp:~methanal-developers/methanal/enum-object-and-group-choices | ||||||||
Merge into: | lp:methanal | ||||||||
Diff against target: |
1424 lines (+849/-170) 11 files modified
methanal/enums.py (+94/-52) methanal/imethanal.py (+49/-0) methanal/js/Methanal/View.js (+1/-1) methanal/static/styles/methanal.css (+11/-0) methanal/test/test_enums.py (+246/-64) methanal/test/test_view.py (+303/-5) methanal/themes/methanal-base/methanal-multicheck-input.html (+3/-0) methanal/themes/methanal-base/methanal-multiselect-input.html (+1/-5) methanal/themes/methanal-base/methanal-radio-input.html (+10/-7) methanal/themes/methanal-base/methanal-select-input.html (+0/-1) methanal/view.py (+131/-35) |
||||||||
To merge this branch: | bzr merge lp:~methanal-developers/methanal/enum-object-and-group-choices | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tristan Seligmann | Approve | ||
Review via email:
|
Commit message
Description of the change
Implement ObjectEnum for using arbitrary Python objects with ChoiceInputs and enhance ChoiceInput to respect a "group" EnumItem extra value for visually grouping enumeration values.
- 145. By Jonathan Jacobs
-
Make Enum.__iter__ respect EnumItem.hidden.
- 146. By Jonathan Jacobs
-
Name tweaks.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jonathan Jacobs (jjacobs) wrote : | # |
- 147. By Jonathan Jacobs
-
Cosmetic tweaks.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tristan Seligmann (mithrandi) wrote : | # |
Regarding your thoughts:
1. It's used several times in Fusion too, so I think let's keep it.
2. The widgets adapt the value passed in to IEnumeration, and then call various methods on that. IEnumeration needs to include all of those methods.
3. If someone needs it later, we can add it then; I'd rather not add a public interface until there's a use case.
Regarding the rest of the branch:
34 + type(theList[0][1]) not in (tuple, list)):
Rather use "not isinstance(
1179 +ObjectRadioGro
1180 + ObjectRadioGrou
Can't this be done using @decorator syntax instead? I think it might be clearer that way.
- 148. By Jonathan Jacobs
-
Move more of Enum into IEnumeration.
- 149. By Jonathan Jacobs
-
Improve Enum.__repr__.
- 150. By Jonathan Jacobs
-
Deprecate ListEnumeration.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jonathan Jacobs (jjacobs) wrote : | # |
> Regarding the rest of the branch:
>
> 34 + type(theList[0][1]) not in (tuple, list)):
>
> Rather use "not isinstance(
> deprecate ListEnumeration? It's really awful.
Done.
There are piles of deprecation warnings now, but I don't think its worth trying to clean them up since in the next version of Methanal we'll be deleting most of the associated code and the tests will have to change (to use Enum instead of lists) in any case.
- 151. By Jonathan Jacobs
-
Merge forward.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tristan Seligmann (mithrandi) : | # |
Preview Diff
1 | === modified file 'methanal/enums.py' |
2 | --- methanal/enums.py 2009-09-20 18:55:19 +0000 |
3 | +++ methanal/enums.py 2010-07-13 11:45:57 +0000 |
4 | @@ -1,24 +1,33 @@ |
5 | +import textwrap |
6 | from zope.interface import implements |
7 | |
8 | +from twisted.python.versions import Version |
9 | +from twisted.python.deprecate import deprecated |
10 | + |
11 | from methanal.errors import InvalidEnumItem |
12 | from methanal.imethanal import IEnumeration |
13 | |
14 | |
15 | |
16 | -class ListEnumeration(object): |
17 | - """ |
18 | - An L{IEnumeration} implementation for the C{list} type. |
19 | - """ |
20 | - implements(IEnumeration) |
21 | - |
22 | - |
23 | - def __init__(self, theList): |
24 | - self.theList = theList |
25 | - |
26 | - |
27 | - # IEnumeration |
28 | - def asPairs(self): |
29 | - return self.theList |
30 | +@deprecated(Version('methanal', 0, 2, 1)) |
31 | +def ListEnumeration(theList): |
32 | + """ |
33 | + An L{IEnumeration} adapter for the C{list} type. |
34 | + |
35 | + This is deprecated, use L{Enum.fromPairs} instead. |
36 | + """ |
37 | + # If this isn't a grouped input, turn it into one with one unnamed group. |
38 | + if (theList and |
39 | + len(theList[0]) > 1 and |
40 | + not isinstance(theList[0][1], (tuple, list))): |
41 | + theList = [(None, theList)] |
42 | + |
43 | + items = [] |
44 | + for groupName, values in theList: |
45 | + for value, desc in values: |
46 | + items.append(EnumItem(value, desc, group=groupName)) |
47 | + |
48 | + return Enum('', items) |
49 | |
50 | |
51 | |
52 | @@ -26,7 +35,7 @@ |
53 | """ |
54 | An enumeration. |
55 | |
56 | - @ivar doc: A brief description of the enumeration's intent |
57 | + L{Enum} objects implement the iterator protocol. |
58 | |
59 | @ivar _order: A list of enumeration items, used to preserve the original |
60 | order of the enumeration |
61 | @@ -40,20 +49,45 @@ |
62 | """ |
63 | Initialise an enumeration. |
64 | |
65 | - @param doc: See L{Enum.doc} |
66 | - |
67 | - @type values: C{iterable} of L{EnumItem} |
68 | + @type values: C{iterable} of L{EnumItem} |
69 | """ |
70 | self.doc = doc |
71 | |
72 | _order = self._order = [] |
73 | _values = self._values = {} |
74 | for value in values: |
75 | - if value.value in _values: |
76 | + key = self._getValueMapping(value) |
77 | + if key in _values: |
78 | raise ValueError( |
79 | - '%r is already a value in the enumeration' % (value.value,)) |
80 | + '%r is already a value in the enumeration' % (key,)) |
81 | _order.append(value) |
82 | - _values[value.value] = value |
83 | + _values[key] = value |
84 | + |
85 | + |
86 | + def __iter__(self): |
87 | + for item in self._order: |
88 | + if not item.hidden: |
89 | + yield item |
90 | + |
91 | + |
92 | + def __repr__(self): |
93 | + lines = textwrap.wrap(textwrap.dedent(self.doc.strip())) |
94 | + line = 'undocumented' |
95 | + if lines: |
96 | + line = lines[0] |
97 | + if len(lines) > 1: |
98 | + line += '...' |
99 | + line = '"""%s"""' % (line,) |
100 | + return '<%s %s>' % ( |
101 | + type(self).__name__, |
102 | + line) |
103 | + |
104 | + |
105 | + def _getValueMapping(self, value): |
106 | + """ |
107 | + Determine the key to use when constructing a mapping for C{value}. |
108 | + """ |
109 | + return value.value |
110 | |
111 | |
112 | @classmethod |
113 | @@ -71,12 +105,9 @@ |
114 | return cls(doc=doc, values=values) |
115 | |
116 | |
117 | + # IEnumeration |
118 | + |
119 | def get(self, value): |
120 | - """ |
121 | - Get an enumeration item for a given enumeration value. |
122 | - |
123 | - @rtype: L{EnumItem} |
124 | - """ |
125 | item = self._values.get(value) |
126 | if item is None: |
127 | raise InvalidEnumItem(value) |
128 | @@ -84,9 +115,6 @@ |
129 | |
130 | |
131 | def getDesc(self, value): |
132 | - """ |
133 | - Get the description for a given enumeration value. |
134 | - """ |
135 | try: |
136 | return self.get(value).desc |
137 | except InvalidEnumItem: |
138 | @@ -94,9 +122,6 @@ |
139 | |
140 | |
141 | def getExtra(self, value, extraName, default=None): |
142 | - """ |
143 | - Get the extra value for C{extraName} or use C{default}. |
144 | - """ |
145 | try: |
146 | return self.get(value).get(extraName, default) |
147 | except InvalidEnumItem: |
148 | @@ -104,43 +129,52 @@ |
149 | |
150 | |
151 | def find(self, **names): |
152 | - """ |
153 | - Find the first L{EnumItem} with matching extra values. |
154 | - |
155 | - @param **names: Extra values to match |
156 | - |
157 | - @rtype: L{EnumItem} |
158 | - @return: The first matching L{EnumItem} or C{None} if there are no |
159 | - matches |
160 | - """ |
161 | for res in self.findAll(**names): |
162 | return res |
163 | return None |
164 | |
165 | |
166 | def findAll(self, **names): |
167 | - """ |
168 | - Find all L{EnumItem}s with matching extra values. |
169 | - |
170 | - @param **names: Extra values to match |
171 | - |
172 | - @rtype: C{iterable} of L{EnumItem} |
173 | - """ |
174 | values = names.items() |
175 | if len(values) != 1: |
176 | raise ValueError('Only one query is allowed at a time') |
177 | |
178 | name, value = values[0] |
179 | - for item in self._order: |
180 | + for item in self: |
181 | if item.get(name) == value: |
182 | yield item |
183 | |
184 | |
185 | + def asPairs(self): |
186 | + return [(i.value, i.desc) for i in self] |
187 | + |
188 | + |
189 | + |
190 | +class ObjectEnum(Enum): |
191 | + """ |
192 | + An enumeration for arbitrary Python objects. |
193 | + |
194 | + Pass the Python object as the C{value} parameter to L{EnumItem}. |
195 | + C{ObjectEnum} will automatically create an C{'id'} extra value for |
196 | + L{EnumItem}s that do not already have such a value. |
197 | + """ |
198 | + def _getValueMapping(self, value): |
199 | + key = unicode(id(value.value)) |
200 | + if value.get('id') is None: |
201 | + value._extra['id'] = key |
202 | + return key |
203 | + |
204 | + |
205 | + def get(self, value): |
206 | + value = unicode(id(value)) |
207 | + return super(ObjectEnum, self).get(value) |
208 | + |
209 | + |
210 | # IEnumeration |
211 | + |
212 | def asPairs(self): |
213 | - return [(i.value, i.desc) |
214 | - for i in self._order |
215 | - if not i.hidden] |
216 | + return [(i.id, i.desc) |
217 | + for i in self] |
218 | |
219 | |
220 | |
221 | @@ -170,6 +204,14 @@ |
222 | self._extra = extra |
223 | |
224 | |
225 | + def __repr__(self): |
226 | + return '<%s value=%r desc=%r hidden=%r>' % ( |
227 | + type(self).__name__, |
228 | + self.value, |
229 | + self.desc, |
230 | + self.hidden) |
231 | + |
232 | + |
233 | def __getattr__(self, name): |
234 | """ |
235 | Get an extra value by name. |
236 | |
237 | === modified file 'methanal/imethanal.py' |
238 | --- methanal/imethanal.py 2009-11-03 22:23:50 +0000 |
239 | +++ methanal/imethanal.py 2010-07-13 11:45:57 +0000 |
240 | @@ -62,6 +62,10 @@ |
241 | """ |
242 | An enumeration. |
243 | """ |
244 | + doc = Attribute(""" |
245 | + A brief description of the enumeration's intent. |
246 | + """) |
247 | + |
248 | def asPairs(): |
249 | """ |
250 | Represent the enumeration as a sequence of pairs. |
251 | @@ -69,3 +73,48 @@ |
252 | @rtype: C{list} of 2-C{tuple}s |
253 | @return: A sequence of C{(value, description)} |
254 | """ |
255 | + |
256 | + |
257 | + def get(value): |
258 | + """ |
259 | + Get an enumeration item for a given enumeration value. |
260 | + |
261 | + @raise L{methanal.errors.InvalidEnumItem}: If C{value} does not match |
262 | + any known enumeration value. |
263 | + |
264 | + @rtype: L{EnumItem} |
265 | + """ |
266 | + |
267 | + |
268 | + def getDesc(value): |
269 | + """ |
270 | + Get the description for a given enumeration value. |
271 | + """ |
272 | + |
273 | + |
274 | + def getExtra(value, extraName, default=None): |
275 | + """ |
276 | + Get the extra value for C{extraName} or use C{default}. |
277 | + """ |
278 | + |
279 | + |
280 | + def find(**names): |
281 | + """ |
282 | + Find the first L{EnumItem} with matching extra values. |
283 | + |
284 | + @param **names: Extra values to match |
285 | + |
286 | + @rtype: L{EnumItem} |
287 | + @return: The first matching L{EnumItem} or C{None} if there are no |
288 | + matches |
289 | + """ |
290 | + |
291 | + |
292 | + def findAll(**names): |
293 | + """ |
294 | + Find all L{EnumItem}s with matching extra values. |
295 | + |
296 | + @param **names: Extra values to match |
297 | + |
298 | + @rtype: C{iterable} of L{EnumItem} |
299 | + """ |
300 | |
301 | === modified file 'methanal/js/Methanal/View.js' |
302 | --- methanal/js/Methanal/View.js 2010-07-08 19:22:54 +0000 |
303 | +++ methanal/js/Methanal/View.js 2010-07-13 11:45:57 +0000 |
304 | @@ -1827,7 +1827,7 @@ |
305 | */ |
306 | Methanal.View.FormInput.subclass(Methanal.View, 'MultiInputBase').methods( |
307 | function getInputNode(self) { |
308 | - return self.getInputNodes()[0]; |
309 | + return self.node.getElementsByTagName('input')[0]; |
310 | }, |
311 | |
312 | |
313 | |
314 | === modified file 'methanal/static/styles/methanal.css' |
315 | --- methanal/static/styles/methanal.css 2010-07-08 18:27:35 +0000 |
316 | +++ methanal/static/styles/methanal.css 2010-07-13 11:45:57 +0000 |
317 | @@ -562,6 +562,17 @@ |
318 | display: none !important; |
319 | } |
320 | |
321 | +.methanal-choice-group { |
322 | + border: solid #ddd; |
323 | + border-width: 0 0 0 1px; |
324 | + margin: 0.5em; |
325 | + padding: 0.3em 0 0.3em 0.5em; |
326 | +} |
327 | + |
328 | +.methanal-choice-group h4 { |
329 | + margin: 0 0 0.25em 0; |
330 | +} |
331 | + |
332 | .dependancy-parent { |
333 | border-left: 3px solid #6094a4; |
334 | margin-left: -3px; |
335 | |
336 | === modified file 'methanal/test/test_enums.py' |
337 | --- methanal/test/test_enums.py 2009-12-05 16:01:59 +0000 |
338 | +++ methanal/test/test_enums.py 2010-07-13 11:45:57 +0000 |
339 | @@ -15,31 +15,46 @@ |
340 | """ |
341 | def test_list(self): |
342 | """ |
343 | - Adapting a C{list} to L{IEnumeration} results in an identical list. |
344 | + Adapting a C{list} to L{IEnumeration} results in an L{Enum} accurately |
345 | + representing the list. |
346 | """ |
347 | values = [ |
348 | (u'foo', u'Foo'), |
349 | (u'bar', u'Bar')] |
350 | - self.assertEquals(IEnumeration(values).asPairs(), values) |
351 | + enum = IEnumeration(values) |
352 | + self.assertEquals(enum.asPairs(), values) |
353 | + for value, desc in values: |
354 | + item = enum.get(value) |
355 | + self.assertEquals(item.value, value) |
356 | + self.assertEquals(item.desc, desc) |
357 | |
358 | |
359 | def test_groupList(self): |
360 | """ |
361 | Adapting a C{list} of nested C{list}s, as used by |
362 | - L{methanal.view.GroupedSelectInput}, results in an identical list. |
363 | + L{methanal.view.GroupedSelectInput}, results in an L{Enum} with |
364 | + L{EnumItems} with a C{'group'} extra value the same as the first |
365 | + element in each C{tuple}. L{IEnumeration.asPairs} returns a flat |
366 | + C{list} for nested C{list}s adapted to L{IEnumeration}. |
367 | """ |
368 | values = [ |
369 | (u'Group', [ |
370 | (u'foo', u'Foo'), |
371 | (u'bar', u'Bar')]), |
372 | - (u'Group', [ |
373 | + (u'Group 2', [ |
374 | (u'quux', u'Quux'), |
375 | (u'frob', u'Frob')])] |
376 | |
377 | - pairs = IEnumeration(values).asPairs() |
378 | - for i, (groupName, innerValues) in enumerate(pairs): |
379 | - self.assertEquals(groupName, u'Group') |
380 | - self.assertEquals(pairs[i][1], innerValues) |
381 | + enum = IEnumeration(values) |
382 | + for groupName, innerValues in values: |
383 | + for value, desc in innerValues: |
384 | + item = enum.get(value) |
385 | + self.assertEquals(item.value, value) |
386 | + self.assertEquals(item.desc, desc) |
387 | + self.assertEquals(item.get('group'), groupName) |
388 | + |
389 | + pairs = sum(zip(*values)[1], []) |
390 | + self.assertEquals(enum.asPairs(), pairs) |
391 | |
392 | |
393 | def test_notAdapted(self): |
394 | @@ -53,18 +68,10 @@ |
395 | |
396 | |
397 | |
398 | -class EnumTests(unittest.TestCase): |
399 | - """ |
400 | - Tests for L{methanal.enums.Enum}. |
401 | - """ |
402 | - def setUp(self): |
403 | - self.values = [ |
404 | - enums.EnumItem(u'foo', u'Foo', quux=u'hello', frob=u'world'), |
405 | - enums.EnumItem(u'bar', u'Bar', quux=u'goodbye'), |
406 | - enums.EnumItem(u'doh', u'Doh', frob=u'world')] |
407 | - self.enum = enums.Enum('Doc', self.values) |
408 | - |
409 | - |
410 | +class _EnumTestsMixin(object): |
411 | + """ |
412 | + Test case mixin for enumerations. |
413 | + """ |
414 | def test_duplicateValues(self): |
415 | """ |
416 | Constructing an enumeration with duplicate values results in |
417 | @@ -96,7 +103,7 @@ |
418 | """ |
419 | Representing an enumeration as a list of pairs. |
420 | """ |
421 | - pairs = [(e.value, e.desc) for e in self.values] |
422 | + pairs = [(e.get('id', e.value), e.desc) for e in self.values] |
423 | self.assertEquals(self.enum.asPairs(), pairs) |
424 | |
425 | |
426 | @@ -123,45 +130,6 @@ |
427 | self.assertEquals(self.enum.getDesc(u'DOESNOTEXIST'), u'') |
428 | |
429 | |
430 | - def test_getExtra(self): |
431 | - """ |
432 | - Getting an enumeration item extra value by enumeration value returns |
433 | - the extra's value or a default value, defaulting to C{None}. |
434 | - """ |
435 | - self.assertEquals(self.enum.getExtra(u'foo', 'quux'), u'hello') |
436 | - self.assertEquals(self.enum.getExtra(u'foo', 'frob'), u'world') |
437 | - self.assertEquals(self.enum.getExtra(u'bar', 'quux'), u'goodbye') |
438 | - |
439 | - self.assertEquals(self.enum.getExtra(u'bar', 'nope'), None) |
440 | - self.assertEquals(self.enum.getExtra(u'bar', 'nope', u''), u'') |
441 | - |
442 | - |
443 | - def test_extra(self): |
444 | - """ |
445 | - Extra parameters are retrieved by L{methanal.enums.EnumItem.get} if they |
446 | - exist otherwise a default value is returned instead. Extra parameters |
447 | - can also be accessed via attribute access but C{AttributeError} is |
448 | - raised if no such extra parameter exists. |
449 | - """ |
450 | - self.assertEquals(self.enum.get(u'foo').get('quux'), u'hello') |
451 | - self.assertEquals(self.enum.get(u'foo').get('frob'), u'world') |
452 | - self.assertEquals(self.enum.get(u'bar').get('quux'), u'goodbye') |
453 | - |
454 | - self.assertEquals(self.enum.get(u'bar').get('boop'), None) |
455 | - self.assertEquals(self.enum.get(u'bar').get('beep', 42), 42) |
456 | - |
457 | - self.assertEquals(self.enum.get(u'foo').quux, u'hello') |
458 | - self.assertEquals(self.enum.get(u'foo').frob, u'world') |
459 | - self.assertEquals(self.enum.get(u'bar').quux, u'goodbye') |
460 | - |
461 | - e = self.assertRaises(AttributeError, |
462 | - getattr, self.enum.get(u'bar'), 'boop') |
463 | - self.assertIn('boop', str(e)) |
464 | - e = self.assertRaises(AttributeError, |
465 | - getattr, self.enum.get(u'bar'), 'beep') |
466 | - self.assertIn('beep', str(e)) |
467 | - |
468 | - |
469 | def test_hidden(self): |
470 | """ |
471 | Enumeration items that have their C{hidden} flag set are not listed in |
472 | @@ -184,11 +152,11 @@ |
473 | or C{None} if there are no matches. Passing fewer or more than one |
474 | query raises C{ValueError}. |
475 | """ |
476 | - self.assertEquals(self.enum.find(quux=u'hello'), self.values[0]) |
477 | - self.assertEquals(self.enum.find(frob=u'world'), self.values[0]) |
478 | - self.assertEquals(self.enum.find(quux=u'goodbye'), self.values[1]) |
479 | + self.assertIdentical(self.enum.find(quux=u'hello'), self.values[0]) |
480 | + self.assertIdentical(self.enum.find(frob=u'world'), self.values[0]) |
481 | + self.assertIdentical(self.enum.find(quux=u'goodbye'), self.values[1]) |
482 | |
483 | - self.assertEquals(self.enum.find(haha=u'nothanks'), None) |
484 | + self.assertIdentical(self.enum.find(haha=u'nothanks'), None) |
485 | self.assertRaises(ValueError, self.enum.find) |
486 | self.assertRaises(ValueError, self.enum.find, foo=u'foo', bar=u'bar') |
487 | |
488 | @@ -210,3 +178,217 @@ |
489 | self.assertRaises(ValueError, list, self.enum.findAll()) |
490 | self.assertRaises(ValueError, list, self.enum.findAll(foo=u'foo', |
491 | bar=u'bar')) |
492 | + |
493 | + |
494 | + def test_iterator(self): |
495 | + """ |
496 | + L{Enum} implements the iterator protocol and will iterate over |
497 | + L{EnumItem}s in the order originally specified, omitting L{EnumItem}s |
498 | + that are marked as hidden. |
499 | + """ |
500 | + items = [enums.EnumItem(u'foo', u'Foo'), |
501 | + enums.EnumItem(u'bar', u'Bar'), |
502 | + enums.EnumItem(u'baz', u'Baz', hidden=True)] |
503 | + enum = enums.Enum('Doc', items) |
504 | + |
505 | + # The hidden Enum is omitted. |
506 | + self.assertEquals(len(list(enum)), 2) |
507 | + |
508 | + for expected, item in zip(items, enum): |
509 | + self.assertIdentical(expected, item) |
510 | + |
511 | + |
512 | + |
513 | +class EnumTests(_EnumTestsMixin, unittest.TestCase): |
514 | + """ |
515 | + Tests for L{methanal.enums.Enum}. |
516 | + """ |
517 | + def setUp(self): |
518 | + self.values = [ |
519 | + enums.EnumItem(u'foo', u'Foo', quux=u'hello', frob=u'world'), |
520 | + enums.EnumItem(u'bar', u'Bar', quux=u'goodbye'), |
521 | + enums.EnumItem(u'doh', u'Doh', frob=u'world')] |
522 | + self.enum = enums.Enum('Doc', self.values) |
523 | + |
524 | + |
525 | + def test_getExtra(self): |
526 | + """ |
527 | + Getting an enumeration item extra value by enumeration value returns |
528 | + the extra's value or a default value, defaulting to C{None}. |
529 | + """ |
530 | + self.assertEquals(self.enum.getExtra(u'foo', 'quux'), u'hello') |
531 | + self.assertEquals(self.enum.getExtra(u'foo', 'frob'), u'world') |
532 | + self.assertEquals(self.enum.getExtra(u'bar', 'quux'), u'goodbye') |
533 | + |
534 | + self.assertEquals(self.enum.getExtra(u'bar', 'nope'), None) |
535 | + self.assertEquals(self.enum.getExtra(u'bar', 'nope', u''), u'') |
536 | + |
537 | + |
538 | + def test_extra(self): |
539 | + """ |
540 | + Extra parameters are retrieved by L{methanal.enums.EnumItem.get} if they |
541 | + exist otherwise a default value is returned instead. Extra parameters |
542 | + can also be accessed via attribute access but C{AttributeError} is |
543 | + raised if no such extra parameter exists. |
544 | + """ |
545 | + self.assertEquals(self.enum.get(u'foo').get('quux'), u'hello') |
546 | + self.assertEquals(self.enum.get(u'foo').get('frob'), u'world') |
547 | + self.assertEquals(self.enum.get(u'bar').get('quux'), u'goodbye') |
548 | + |
549 | + self.assertEquals(self.enum.get(u'bar').get('boop'), None) |
550 | + self.assertEquals(self.enum.get(u'bar').get('beep', 42), 42) |
551 | + |
552 | + self.assertEquals(self.enum.get(u'foo').quux, u'hello') |
553 | + self.assertEquals(self.enum.get(u'foo').frob, u'world') |
554 | + self.assertEquals(self.enum.get(u'bar').quux, u'goodbye') |
555 | + |
556 | + e = self.assertRaises(AttributeError, |
557 | + getattr, self.enum.get(u'bar'), 'boop') |
558 | + self.assertIn('boop', str(e)) |
559 | + e = self.assertRaises(AttributeError, |
560 | + getattr, self.enum.get(u'bar'), 'beep') |
561 | + self.assertIn('beep', str(e)) |
562 | + |
563 | + |
564 | + def test_reprEnum(self): |
565 | + """ |
566 | + L{methanal.enums.Enum} has a useful representation that contains the |
567 | + type name and the enumeration description. |
568 | + """ |
569 | + self.assertEquals( |
570 | + repr(enums.Enum('Foo bar', [])), |
571 | + '<Enum """Foo bar""">') |
572 | + |
573 | + lorem = """ |
574 | + Lorem ipsum dolor sit amet, consectetur adipiscing elit. In vitae sem |
575 | + felis, sit amet tincidunt est. Cras convallis, odio nec accumsan |
576 | + vestibulum, lectus dolor feugiat magna, sit amet tempus lorem diam ac |
577 | + enim. Curabitur nisl nibh, bibendum ac tempus non, blandit ac turpis. |
578 | + """ |
579 | + |
580 | + self.assertEquals( |
581 | + repr(enums.Enum(lorem, [])), |
582 | + '<Enum """Lorem ipsum dolor sit amet, consectetur adipiscing elit.' |
583 | + ' In vitae sem...""">') |
584 | + |
585 | + |
586 | + def test_reprEnumUndocumented(self): |
587 | + """ |
588 | + L{methanal.enums.Enum} has a useful representation even when the |
589 | + enumeration has no description. |
590 | + """ |
591 | + self.assertEquals( |
592 | + repr(enums.Enum('', [])), |
593 | + '<Enum undocumented>') |
594 | + |
595 | + |
596 | + |
597 | +class EnumItemTests(unittest.TestCase): |
598 | + """ |
599 | + Tests for L{methanal.enums.EnumItem}. |
600 | + """ |
601 | + def test_reprEnumItem(self): |
602 | + """ |
603 | + L{methanal.enums.EnumItem} has a useful representation that contains |
604 | + the value, description and hidden state. |
605 | + """ |
606 | + self.assertEquals( |
607 | + repr(enums.EnumItem(u'foo', u'Foo')), |
608 | + "<EnumItem value=u'foo' desc=u'Foo' hidden=False>") |
609 | + |
610 | + self.assertEquals( |
611 | + repr(enums.EnumItem(u'foo', u'Foo', hidden=True)), |
612 | + "<EnumItem value=u'foo' desc=u'Foo' hidden=True>") |
613 | + |
614 | + |
615 | + |
616 | +class ObjectEnumTests(_EnumTestsMixin, unittest.TestCase): |
617 | + """ |
618 | + Tests for L{methanal.enums.ObjectEnum}. |
619 | + """ |
620 | + def setUp(self): |
621 | + self.object1 = object() |
622 | + self.object2 = object() |
623 | + self.object3 = object() |
624 | + self.values = [ |
625 | + enums.EnumItem(self.object1, u'Foo', quux=u'hello', frob=u'world'), |
626 | + enums.EnumItem(self.object2, u'Bar', quux=u'goodbye'), |
627 | + enums.EnumItem(self.object3, u'Doh', frob=u'world', id=u'chuck')] |
628 | + self.enum = enums.ObjectEnum('Doc', self.values) |
629 | + |
630 | + |
631 | + def test_idExtra(self): |
632 | + """ |
633 | + L{methanal.enums.ObjectEnum} automatically creates an C{'id'} EnumItem |
634 | + extra value, based on the result of C{id}, if one does not already |
635 | + exist. |
636 | + """ |
637 | + expected = [ |
638 | + unicode(id(self.object1)), |
639 | + unicode(id(self.object2)), |
640 | + u'chuck'] |
641 | + |
642 | + self.assertEquals( |
643 | + expected, |
644 | + [value.id for value in self.values]) |
645 | + |
646 | + |
647 | + def test_getExtra(self): |
648 | + """ |
649 | + Getting an enumeration item extra value by enumeration value returns |
650 | + the extra's value or a default value, defaulting to C{None}. |
651 | + """ |
652 | + self.assertEquals(self.enum.getExtra(self.object1, 'quux'), u'hello') |
653 | + self.assertEquals(self.enum.getExtra(self.object1, 'frob'), u'world') |
654 | + self.assertEquals(self.enum.getExtra(self.object2, 'quux'), u'goodbye') |
655 | + |
656 | + self.assertEquals(self.enum.getExtra(u'bar', 'nope'), None) |
657 | + self.assertEquals(self.enum.getExtra(u'bar', 'nope', u''), u'') |
658 | + |
659 | + |
660 | + def test_extra(self): |
661 | + """ |
662 | + Extra parameters are retrieved by L{methanal.enums.EnumItem.get} if they |
663 | + exist otherwise a default value is returned instead. Extra parameters |
664 | + can also be accessed via attribute access but C{AttributeError} is |
665 | + raised if no such extra parameter exists. |
666 | + """ |
667 | + self.assertEquals(self.enum.get(self.object1).get('quux'), u'hello') |
668 | + self.assertEquals(self.enum.get(self.object1).get('frob'), u'world') |
669 | + self.assertEquals(self.enum.get(self.object2).get('quux'), u'goodbye') |
670 | + |
671 | + self.assertEquals(self.enum.get(self.object2).get('boop'), None) |
672 | + self.assertEquals(self.enum.get(self.object2).get('beep', 42), 42) |
673 | + |
674 | + self.assertEquals(self.enum.get(self.object1).quux, u'hello') |
675 | + self.assertEquals(self.enum.get(self.object1).frob, u'world') |
676 | + self.assertEquals(self.enum.get(self.object2).quux, u'goodbye') |
677 | + |
678 | + e = self.assertRaises(AttributeError, |
679 | + getattr, self.enum.get(self.object2), 'boop') |
680 | + self.assertIn('boop', str(e)) |
681 | + e = self.assertRaises(AttributeError, |
682 | + getattr, self.enum.get(self.object2), 'beep') |
683 | + self.assertIn('beep', str(e)) |
684 | + |
685 | + |
686 | + def test_reprEnum(self): |
687 | + """ |
688 | + L{methanal.enums.ObjectEnum} has a useful representation that contains the |
689 | + type name and the enumeration description. |
690 | + """ |
691 | + self.assertEquals( |
692 | + repr(enums.ObjectEnum('Foo bar', [])), |
693 | + '<ObjectEnum """Foo bar""">') |
694 | + |
695 | + lorem = """ |
696 | + Lorem ipsum dolor sit amet, consectetur adipiscing elit. In vitae sem |
697 | + felis, sit amet tincidunt est. Cras convallis, odio nec accumsan |
698 | + vestibulum, lectus dolor feugiat magna, sit amet tempus lorem diam ac |
699 | + enim. Curabitur nisl nibh, bibendum ac tempus non, blandit ac turpis. |
700 | + """ |
701 | + |
702 | + self.assertEquals( |
703 | + repr(enums.ObjectEnum(lorem, [])), |
704 | + '<ObjectEnum """Lorem ipsum dolor sit amet, consectetur adipiscing elit.' |
705 | + ' In vitae sem...""">') |
706 | |
707 | === modified file 'methanal/test/test_view.py' |
708 | --- methanal/test/test_view.py 2010-03-12 15:21:05 +0000 |
709 | +++ methanal/test/test_view.py 2010-07-13 11:45:57 +0000 |
710 | @@ -19,9 +19,10 @@ |
711 | from nevow import athena, loaders |
712 | from nevow.testutil import renderLivePage |
713 | |
714 | +from methanal import view, errors |
715 | from methanal.imethanal import IEnumeration |
716 | from methanal.model import ItemModel, Value, DecimalValue, Model |
717 | -from methanal import view |
718 | +from methanal.enums import Enum, EnumItem, ObjectEnum |
719 | |
720 | |
721 | |
722 | @@ -571,14 +572,15 @@ |
723 | (TypeError, dict())] |
724 | |
725 | |
726 | - def createControl(self, args): |
727 | + def createControl(self, args, checkExpectedValues=True): |
728 | """ |
729 | Create a L{methanal.view.ChoiceInput} from C{values}, assert that |
730 | L{methanal.view.ChoiceInput.value} provides L{IEnumeration} and |
731 | calling C{asPairs} results in the same values as C{values}. |
732 | """ |
733 | control = super(ChoiceInputTests, self).createControl(args) |
734 | - self.assertEquals(control.values.asPairs(), list(args.get('values'))) |
735 | + if checkExpectedValues: |
736 | + self.assertEquals(control.values.asPairs(), list(args.get('values'))) |
737 | return control |
738 | |
739 | |
740 | @@ -592,7 +594,284 @@ |
741 | (u'foo', u'Foo'), |
742 | (u'bar', u'Bar')]) |
743 | self.createControl(dict(values=values)) |
744 | - self.assertEquals(len(self.flushWarnings()), 1) |
745 | + self.assertEquals(len(self.flushWarnings()), 2) |
746 | + |
747 | + |
748 | + |
749 | +class SelectInputTests(ChoiceInputTests): |
750 | + """ |
751 | + Tests for L{methanal.view.SelectInput}. |
752 | + """ |
753 | + controlType = view.SelectInput |
754 | + |
755 | + def test_renderGroupsOptions(self): |
756 | + """ |
757 | + The options of a C{SelectInput} are rendered as groups if the enumeration |
758 | + values specify a C{'group'} extra value. |
759 | + """ |
760 | + values = Enum( |
761 | + '', |
762 | + [EnumItem(u'foo', u'Foo', group=u'Group'), |
763 | + EnumItem(u'bar', u'Bar', group=u'Group')]) |
764 | + |
765 | + control = self.createControl( |
766 | + dict(values=values), checkExpectedValues=False) |
767 | + |
768 | + def verifyRendering(tree): |
769 | + groupNodes = tree.findall('//select/optgroup') |
770 | + groups = set(item.group for item in values) |
771 | + self.assertEquals(len(groupNodes), len(groups)) |
772 | + for groupNode, item in zip(groupNodes, values): |
773 | + self.assertEquals(groupNode.get('label'), item.group) |
774 | + |
775 | + optionNodes = groupNode.findall('option') |
776 | + self.assertEquals(len(optionNodes), len(list(values))) |
777 | + for optionNode, innerItem in zip(optionNodes, values): |
778 | + self.assertEquals(optionNode.get('value'), innerItem.value) |
779 | + self.assertEquals(optionNode.text.strip(), innerItem.desc) |
780 | + |
781 | + return renderWidget(control).addCallback(verifyRendering) |
782 | + |
783 | + |
784 | + def test_renderObjectOptions(self): |
785 | + """ |
786 | + If a C{SelectInput}'s values are an C{ObjectEnum} then the rendered |
787 | + option values match the C{'id'} extra of the object enumeration. |
788 | + """ |
789 | + object1 = object() |
790 | + object2 = object() |
791 | + values = ObjectEnum( |
792 | + '', |
793 | + [EnumItem(object1, u'Foo'), |
794 | + EnumItem(object2, u'Bar', id=u'chuck')]) |
795 | + |
796 | + control = self.createControl( |
797 | + dict(values=values), checkExpectedValues=False) |
798 | + |
799 | + def verifyRendering(tree): |
800 | + optionNodes = tree.findall('//option') |
801 | + self.assertEquals(len(optionNodes), len(list(values))) |
802 | + for optionNode, item in zip(optionNodes, values): |
803 | + self.assertEquals(optionNode.get('value'), item.id) |
804 | + self.assertEquals(optionNode.text.strip(), item.desc) |
805 | + |
806 | + return renderWidget(control).addCallback(verifyRendering) |
807 | + |
808 | + |
809 | + def test_getValueObjectEnum(self): |
810 | + """ |
811 | + L{SelectInput.getValue} returns the C{'id'} extra value when backed by |
812 | + an C{ObjectEnum}. |
813 | + """ |
814 | + object1 = object() |
815 | + object2 = object() |
816 | + values = ObjectEnum( |
817 | + '', |
818 | + [EnumItem(object1, u'Foo'), |
819 | + EnumItem(object2, u'Bar', id=u'chuck')]) |
820 | + |
821 | + control = self.createControl( |
822 | + dict(values=values), checkExpectedValues=False) |
823 | + |
824 | + param = control.parent.param |
825 | + |
826 | + param.value = None |
827 | + self.assertIdentical(control.getValue(), None) |
828 | + |
829 | + for value in values: |
830 | + param.value = value.value |
831 | + self.assertEquals(control.getValue(), value.id) |
832 | + |
833 | + |
834 | + def test_invokeObjectEnum(self): |
835 | + """ |
836 | + When backed by an C{ObjectEnum}, L{SelectInput.invoke} sets the |
837 | + parameter value to the Python object of L{EnumItem} with C{'id'} extra |
838 | + matching the invocation data value and C{None} in the C{None} case. |
839 | + """ |
840 | + object1 = object() |
841 | + object2 = object() |
842 | + values = ObjectEnum( |
843 | + '', |
844 | + [EnumItem(object1, u'Foo'), |
845 | + EnumItem(object2, u'Bar', id=u'chuck')]) |
846 | + |
847 | + control = self.createControl( |
848 | + dict(values=values), checkExpectedValues=False) |
849 | + |
850 | + param = control.parent.param |
851 | + data = {param.name: None} |
852 | + control.invoke(data) |
853 | + self.assertIdentical(param.value, None) |
854 | + |
855 | + for value in values: |
856 | + data = {param.name: value.id} |
857 | + control.invoke(data) |
858 | + self.assertIdentical(param.value, value.value) |
859 | + |
860 | + |
861 | + def test_invokeObjectEnumTrickery(self): |
862 | + """ |
863 | + L{SelectInput.invoke} falls back to locating enumeration items by value |
864 | + when no C{'id'} extra value matches. However, this can lead to leaking |
865 | + information about the enumeration values in the case of objects such as |
866 | + strings and integers. In case we fallback to an item with a C{'id'} |
867 | + extra, even if it doesn't match, C{InvalidEnumItem} is raised. |
868 | + """ |
869 | + object1 = object() |
870 | + values = ObjectEnum( |
871 | + '', |
872 | + [EnumItem(object1, u'Foo'), |
873 | + EnumItem(u'password', u'Bar', id=u'chuck')]) |
874 | + |
875 | + control = self.createControl( |
876 | + dict(values=values), checkExpectedValues=False) |
877 | + |
878 | + param = control.parent.param |
879 | + |
880 | + data = {param.name: u'password'} |
881 | + self.assertRaises(errors.InvalidEnumItem, |
882 | + control.invoke, data) |
883 | + |
884 | + |
885 | + |
886 | +class MultiValueChoiceInputTestsMixin(object): |
887 | + """ |
888 | + Tests mixin for L{methanal.view.ChoiceInput}s that support multiple values. |
889 | + """ |
890 | + values = [ |
891 | + (u'foo', u'Foo'), |
892 | + (u'bar', u'Bar'), |
893 | + (u'baz', u'Baz')] |
894 | + |
895 | + def test_getValueNoValues(self): |
896 | + """ |
897 | + Multi-value L{ChoiceInput}s return an empty list from C{getValue} when |
898 | + the parameter is C{None}, or empty. |
899 | + """ |
900 | + control = self.createControl(dict(values=self.values)) |
901 | + |
902 | + control.parent.param.value = None |
903 | + self.assertEquals(control.getValue(), []) |
904 | + |
905 | + control.parent.param.value = [] |
906 | + self.assertEquals(control.getValue(), []) |
907 | + |
908 | + |
909 | + def test_getValue(self): |
910 | + """ |
911 | + Multi-value L{ChoiceInput}s return a C{list} of the selected values |
912 | + from C{getValue}. |
913 | + """ |
914 | + control = self.createControl(dict(values=self.values)) |
915 | + param = control.parent.param |
916 | + |
917 | + for value, desc in self.values: |
918 | + param.value = [value] |
919 | + self.assertEquals( |
920 | + control.getValue(), |
921 | + [value]) |
922 | + |
923 | + v1, v2 = [self.values[0][0], self.values[1][0]] |
924 | + param.value = [v1, v2] |
925 | + self.assertEquals( |
926 | + control.getValue(), |
927 | + [v1, v2]) |
928 | + |
929 | + |
930 | + def test_invokeNoValues(self): |
931 | + """ |
932 | + Multi-value L{ChoiceInput}s set their parameter value to an empty list |
933 | + when invoked with C{None}, or empty. |
934 | + """ |
935 | + control = self.createControl(dict(values=self.values)) |
936 | + param = control.parent.param |
937 | + control.invoke({param.name: None}) |
938 | + self.assertEquals(param.value, []) |
939 | + |
940 | + control = self.createControl(dict(values=self.values)) |
941 | + param = control.parent.param |
942 | + control.invoke({param.name: []}) |
943 | + self.assertEquals(param.value, []) |
944 | + |
945 | + |
946 | + def test_invoke(self): |
947 | + """ |
948 | + Multi-value L{ChoiceInput}s set their parameter value to a C{list} |
949 | + containing the values of the invoked data. |
950 | + """ |
951 | + control = self.createControl(dict(values=self.values)) |
952 | + param = control.parent.param |
953 | + |
954 | + data = {param.name: None} |
955 | + control.invoke(data) |
956 | + self.assertEquals(param.value, []) |
957 | + |
958 | + data = {param.name: [None]} |
959 | + control.invoke(data) |
960 | + self.assertEquals(param.value, [None]) |
961 | + |
962 | + for value, desc in self.values: |
963 | + data = {param.name: [value]} |
964 | + control.invoke(data) |
965 | + self.assertEquals(param.value, [value]) |
966 | + |
967 | + v1, v2 = self.values[0][0], self.values[1][0] |
968 | + data = {param.name: [v1, v2]} |
969 | + control.invoke(data) |
970 | + self.assertEquals(param.value, [v1, v2]) |
971 | + |
972 | + |
973 | + def test_invokeObjectEnum(self): |
974 | + """ |
975 | + Invoking a multi-value L{ChoiceInput}s backed by an L{ObjectEnum} sets |
976 | + their parameter value to a C{list} of objects referenced by ID in the |
977 | + invocation data. |
978 | + """ |
979 | + object1 = object() |
980 | + values = ObjectEnum( |
981 | + '', |
982 | + [EnumItem(object1, u'Foo'), |
983 | + EnumItem(u'buzz', u'Buzz', id=u'quux')]) |
984 | + control = self.createControl( |
985 | + dict(values=values), checkExpectedValues=False) |
986 | + param = control.parent.param |
987 | + |
988 | + for value in values: |
989 | + data = {param.name: [value.id]} |
990 | + control.invoke(data) |
991 | + self.assertEquals(param.value, [value.value]) |
992 | + |
993 | + vs = [v.value for v in values] |
994 | + vIDs = [v.id for v in values] |
995 | + data = {param.name: vIDs} |
996 | + control.invoke(data) |
997 | + self.assertEquals(param.value, vs) |
998 | + |
999 | + data = {param.name: [u'buzz']} |
1000 | + self.assertRaises(errors.InvalidEnumItem, control.invoke, data) |
1001 | + |
1002 | + |
1003 | + def test_render(self): |
1004 | + """ |
1005 | + Rendering the control raises no exceptions. |
1006 | + """ |
1007 | + control = self.createControl(dict(values=self.values)) |
1008 | + ds = [] |
1009 | + |
1010 | + def _render(value): |
1011 | + control.parent.param.value = value |
1012 | + ds.append(renderWidget(control)) |
1013 | + |
1014 | + _render(None) |
1015 | + for obj, desc in self.values: |
1016 | + _render([obj]) |
1017 | + return gatherResults(ds) |
1018 | + |
1019 | + |
1020 | + |
1021 | +class MultiSelectInputTests(ChoiceInputTests, MultiValueChoiceInputTestsMixin): |
1022 | + controlType = view.MultiSelectInput |
1023 | |
1024 | |
1025 | |
1026 | @@ -602,6 +881,18 @@ |
1027 | """ |
1028 | controlType = view.GroupedSelectInput |
1029 | |
1030 | + def test_createDeprecated(self): |
1031 | + """ |
1032 | + Passing values that are not adaptable to IEnumeration are converted |
1033 | + to a C{list}, adapted to L{IEnumeration} and a warning is emitted. |
1034 | + """ |
1035 | + # Not a list. |
1036 | + values = tuple([ |
1037 | + (u'foo', u'Foo'), |
1038 | + (u'bar', u'Bar')]) |
1039 | + self.createControl(dict(values=values)) |
1040 | + self.assertEquals(len(self.flushWarnings()), 3) |
1041 | + |
1042 | |
1043 | def test_renderOptions(self): |
1044 | """ |
1045 | @@ -611,7 +902,8 @@ |
1046 | values = [(u'Group', [(u'foo', u'Foo'), |
1047 | (u'bar', u'Bar')])] |
1048 | |
1049 | - control = self.createControl(dict(values=values)) |
1050 | + control = self.createControl( |
1051 | + dict(values=values), checkExpectedValues=False) |
1052 | |
1053 | def verifyRendering(tree): |
1054 | groupNodes = tree.findall('//select/optgroup') |
1055 | @@ -753,6 +1045,12 @@ |
1056 | |
1057 | |
1058 | |
1059 | +class MultiCheckboxInputTests(ChoiceInputTests, |
1060 | + MultiValueChoiceInputTestsMixin): |
1061 | + controlType = view.MultiCheckboxInput |
1062 | + |
1063 | + |
1064 | + |
1065 | class TestItem(Item): |
1066 | """ |
1067 | A test Item with some attributes. |
1068 | |
1069 | === modified file 'methanal/themes/methanal-base/methanal-multicheck-input.html' |
1070 | --- methanal/themes/methanal-base/methanal-multicheck-input.html 2007-05-19 19:13:54 +0000 |
1071 | +++ methanal/themes/methanal-base/methanal-multicheck-input.html 2010-07-13 11:45:57 +0000 |
1072 | @@ -1,6 +1,9 @@ |
1073 | <div class="methanal-input methanal-control-multicheck" xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> |
1074 | <div class="methanal-multi-input"> |
1075 | <nevow:invisible nevow:render="options"> |
1076 | + <div class="methanal-choice-group" nevow:pattern="optgroup"> |
1077 | + <h4><nevow:slot name="label" /></h4> |
1078 | + </div> |
1079 | <div nevow:pattern="option"> |
1080 | <label> |
1081 | <input type="checkbox"> |
1082 | |
1083 | === modified file 'methanal/themes/methanal-base/methanal-multiselect-input.html' |
1084 | --- methanal/themes/methanal-base/methanal-multiselect-input.html 2009-09-09 01:56:42 +0000 |
1085 | +++ methanal/themes/methanal-base/methanal-multiselect-input.html 2010-07-13 11:45:57 +0000 |
1086 | @@ -5,11 +5,7 @@ |
1087 | <optgroup nevow:pattern="optgroup"> |
1088 | <nevow:attr name="label"><nevow:slot name="label" /></nevow:attr> |
1089 | </optgroup> |
1090 | - |
1091 | - <option nevow:pattern="option"> |
1092 | - <nevow:attr name="value"><nevow:slot name="value" /></nevow:attr> |
1093 | - <nevow:slot name="description" /> |
1094 | - </option> |
1095 | + <option nevow:pattern="option"><nevow:attr name="value"><nevow:slot name="value" /></nevow:attr><nevow:slot name="description" /></option> |
1096 | </nevow:invisible> |
1097 | </select> |
1098 | <div class="methanal-multiselect-selection"> |
1099 | |
1100 | === modified file 'methanal/themes/methanal-base/methanal-radio-input.html' |
1101 | --- methanal/themes/methanal-base/methanal-radio-input.html 2010-03-04 10:19:46 +0000 |
1102 | +++ methanal/themes/methanal-base/methanal-radio-input.html 2010-07-13 11:45:57 +0000 |
1103 | @@ -1,13 +1,16 @@ |
1104 | <div class="methanal-input methanal-control-radio" xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> |
1105 | <div class="methanal-multi-input"> |
1106 | <nevow:invisible nevow:render="options"> |
1107 | - <div nevow:pattern="option"> |
1108 | - <label><input type="radio" |
1109 | - ><athena:handler event="onchange" handler="onChange"></athena:handler |
1110 | - ><nevow:attr name="name"><nevow:slot name="name" /></nevow:attr |
1111 | - ><nevow:attr name="value"><nevow:slot name="value" /></nevow:attr |
1112 | - ></input><nevow:slot name="description" /></label> |
1113 | - </div> |
1114 | + <div class="methanal-choice-group" nevow:pattern="optgroup"> |
1115 | + <h4><nevow:slot name="label" /></h4> |
1116 | + </div> |
1117 | + <div nevow:pattern="option"> |
1118 | + <label><input type="radio" |
1119 | + ><athena:handler event="onchange" handler="onChange"></athena:handler |
1120 | + ><nevow:attr name="name"><nevow:slot name="name" /></nevow:attr |
1121 | + ><nevow:attr name="value"><nevow:slot name="value" /></nevow:attr |
1122 | + ></input><nevow:slot name="description" /></label> |
1123 | + </div> |
1124 | </nevow:invisible> |
1125 | </div> |
1126 | <span class="methanal-error" id="error" /> |
1127 | |
1128 | === modified file 'methanal/themes/methanal-base/methanal-select-input.html' |
1129 | --- methanal/themes/methanal-base/methanal-select-input.html 2009-09-28 23:51:41 +0000 |
1130 | +++ methanal/themes/methanal-base/methanal-select-input.html 2010-07-13 11:45:57 +0000 |
1131 | @@ -5,7 +5,6 @@ |
1132 | <optgroup nevow:pattern="optgroup"> |
1133 | <nevow:attr name="label"><nevow:slot name="label" /></nevow:attr> |
1134 | </optgroup> |
1135 | - |
1136 | <option nevow:pattern="option"><nevow:attr name="value"><nevow:slot name="value" /></nevow:attr><nevow:slot name="description" /></option> |
1137 | </nevow:invisible> |
1138 | </select> |
1139 | |
1140 | === modified file 'methanal/view.py' |
1141 | --- methanal/view.py 2010-07-09 13:50:30 +0000 |
1142 | +++ methanal/view.py 2010-07-13 11:45:57 +0000 |
1143 | @@ -1,3 +1,4 @@ |
1144 | +import itertools |
1145 | from warnings import warn |
1146 | |
1147 | from decimal import Decimal |
1148 | @@ -16,6 +17,7 @@ |
1149 | from xmantissa.ixmantissa import IWebTranslator |
1150 | from xmantissa.webtheme import ThemedElement |
1151 | |
1152 | +from methanal import errors |
1153 | from methanal.imethanal import IEnumeration |
1154 | from methanal.model import ItemModel, Model, paramFromAttribute |
1155 | from methanal.util import getArgsDict |
1156 | @@ -732,7 +734,9 @@ |
1157 | Abstract input with multiple options. |
1158 | |
1159 | @type values: L{IEnumeration} |
1160 | - @ivar values: An enumeration to be used for choice options |
1161 | + @ivar values: An enumeration to be used for choice options. C{ChoiceInput} |
1162 | + will group enumeration values by their C{'group'} extra value, if one |
1163 | + exists. |
1164 | """ |
1165 | def __init__(self, values, **kw): |
1166 | super(ChoiceInput, self).__init__(**kw) |
1167 | @@ -744,23 +748,94 @@ |
1168 | self.values = _values |
1169 | |
1170 | |
1171 | + def _makeOptions(self, pattern, enums): |
1172 | + """ |
1173 | + Create "option" elements, based on C{pattern}, from C{enums}. |
1174 | + """ |
1175 | + for enum in enums: |
1176 | + o = pattern() |
1177 | + o.fillSlots('value', enum.get('id', enum.value)) |
1178 | + o.fillSlots('description', enum.desc) |
1179 | + yield o |
1180 | + |
1181 | + |
1182 | @renderer |
1183 | def options(self, req, tag): |
1184 | """ |
1185 | Render all available options. |
1186 | """ |
1187 | - option = tag.patternGenerator('option') |
1188 | - for value, description in self.values.asPairs(): |
1189 | - o = option() |
1190 | - o.fillSlots('value', value) |
1191 | - o.fillSlots('description', description) |
1192 | - yield o |
1193 | + optionPattern = tag.patternGenerator('option') |
1194 | + groupPattern = tag.patternGenerator('optgroup') |
1195 | + |
1196 | + groups = itertools.groupby(self.values, lambda e: e.get('group')) |
1197 | + for group, enums in groups: |
1198 | + options = self._makeOptions(optionPattern, enums) |
1199 | + if group is not None: |
1200 | + g = groupPattern() |
1201 | + g.fillSlots('label', group) |
1202 | + yield g[options] |
1203 | + else: |
1204 | + yield options |
1205 | + |
1206 | + |
1207 | + def _invokeOne(self, value): |
1208 | + if value: |
1209 | + item = self.values.find(id=value) |
1210 | + if item is None: |
1211 | + item = self.values.get(value) |
1212 | + # We got tricked into fetching an enumeration item by value |
1213 | + # instead of id. |
1214 | + if item.get('id') is not None: |
1215 | + raise errors.InvalidEnumItem(value) |
1216 | + value = item.value |
1217 | + return value |
1218 | + |
1219 | + |
1220 | + def invoke(self, data): |
1221 | + """ |
1222 | + Set the model parameter's value from form data. |
1223 | + """ |
1224 | + value = data[self.param.name] |
1225 | + self.param.value = self._invokeOne(value) |
1226 | + |
1227 | + |
1228 | + def _getOneValue(self, value): |
1229 | + if value is None: |
1230 | + return None |
1231 | + item = self.values.get(value) |
1232 | + return item.get('id', item.value) |
1233 | + |
1234 | + |
1235 | + def getValue(self): |
1236 | + """ |
1237 | + Get the model parameter's value. |
1238 | + """ |
1239 | + return self._getOneValue(self.param.value) |
1240 | |
1241 | |
1242 | registerAdapter(ListEnumeration, list, IEnumeration) |
1243 | |
1244 | |
1245 | |
1246 | +class MultiChoiceInputMixin(object): |
1247 | + """ |
1248 | + A mixin for supporting multiple values in a L{ChoiceInput}. |
1249 | + """ |
1250 | + def invoke(self, data): |
1251 | + value = data[self.param.name] |
1252 | + if value is None: |
1253 | + value = [] |
1254 | + self.param.value = map(self._invokeOne, value) |
1255 | + |
1256 | + |
1257 | + def getValue(self): |
1258 | + if self.param.value is None: |
1259 | + return [] |
1260 | + return map(self._getOneValue, self.param.value) |
1261 | + |
1262 | + |
1263 | + |
1264 | +# XXX: All his friends are deprecated, remove this too. |
1265 | class _ObjectChoiceMixinBase(object): |
1266 | """ |
1267 | Common base class for L{ObjectChoiceMixin} and L{ObjectMultiChoiceMixin}. |
1268 | @@ -784,6 +859,20 @@ |
1269 | super(_ObjectChoiceMixinBase, self).__init__(values=objectIDs, **kw) |
1270 | |
1271 | |
1272 | + def invoke(self, data): |
1273 | + """ |
1274 | + Set the model parameter's value from form data. |
1275 | + """ |
1276 | + self.param.value = data[self.param.name] |
1277 | + |
1278 | + |
1279 | + def getValue(self): |
1280 | + """ |
1281 | + Get the model parameter's value. |
1282 | + """ |
1283 | + return self.param.value |
1284 | + |
1285 | + |
1286 | def encodeValue(self, value): |
1287 | """ |
1288 | Encode a Python object's identifier as text. |
1289 | @@ -814,6 +903,7 @@ |
1290 | |
1291 | |
1292 | |
1293 | +# XXX: All his friends are deprecated, remove this too. |
1294 | class ObjectChoiceMixin(_ObjectChoiceMixinBase): |
1295 | """ |
1296 | A mixin for supporting arbitrary Python objects in a L{ChoiceInput}. |
1297 | @@ -828,6 +918,7 @@ |
1298 | |
1299 | |
1300 | |
1301 | +# XXX: All his friends are deprecated, remove this too. |
1302 | class ObjectMultiChoiceMixin(_ObjectChoiceMixinBase): |
1303 | """ |
1304 | A mixin for supporting many arbitrary Python objects in a L{ChoiceInput}. |
1305 | @@ -850,17 +941,10 @@ |
1306 | fragmentName = 'methanal-radio-input' |
1307 | jsClass = u'Methanal.View.RadioGroupInput' |
1308 | |
1309 | - @renderer |
1310 | - def options(self, req, tag): |
1311 | - """ |
1312 | - Render all available options. |
1313 | - """ |
1314 | - option = tag.patternGenerator('option') |
1315 | - for value, description in self.values.asPairs(): |
1316 | - o = option() |
1317 | + def _makeOptions(self, pattern, enums): |
1318 | + options = super(RadioGroupInput, self)._makeOptions(pattern, enums) |
1319 | + for o in options: |
1320 | o.fillSlots('name', self.name) |
1321 | - o.fillSlots('value', value) |
1322 | - o.fillSlots('description', description) |
1323 | yield o |
1324 | |
1325 | |
1326 | @@ -868,11 +952,15 @@ |
1327 | class ObjectRadioGroupInput(ObjectChoiceMixin, RadioGroupInput): |
1328 | """ |
1329 | Variant of L{RadioGroupInput} for arbitrary Python objects. |
1330 | + |
1331 | + Deprecated. Use L{RadioGroupInput} with L{methanal.enums.ObjectEnum}. |
1332 | """ |
1333 | - |
1334 | - |
1335 | - |
1336 | -class MultiCheckboxInput(ChoiceInput): |
1337 | +ObjectRadioGroupInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1338 | + ObjectRadioGroupInput.__init__) |
1339 | + |
1340 | + |
1341 | + |
1342 | +class MultiCheckboxInput(MultiChoiceInputMixin, ChoiceInput): |
1343 | """ |
1344 | Multiple-checkboxes input. |
1345 | """ |
1346 | @@ -884,7 +972,11 @@ |
1347 | class ObjectMultiCheckboxInput(ObjectMultiChoiceMixin, MultiCheckboxInput): |
1348 | """ |
1349 | Variant of L{MultiCheckboxInput} for arbitrary Python objects. |
1350 | + |
1351 | + Deprecated. Use L{MultiCheckboxInput} with L{methanal.enums.ObjectEnum}. |
1352 | """ |
1353 | +ObjectMultiCheckboxInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1354 | + ObjectMultiCheckboxInput.__init__) |
1355 | |
1356 | |
1357 | |
1358 | @@ -900,7 +992,11 @@ |
1359 | class ObjectSelectInput(ObjectChoiceMixin, SelectInput): |
1360 | """ |
1361 | Variant of L{SelectInput} for arbitrary Python objects. |
1362 | + |
1363 | + Deprecated. Use L{SelectInput} with L{methanal.enums.ObjectEnum}. |
1364 | """ |
1365 | +ObjectSelectInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1366 | + ObjectSelectInput.__init__) |
1367 | |
1368 | |
1369 | |
1370 | @@ -913,7 +1009,7 @@ |
1371 | |
1372 | |
1373 | |
1374 | -class MultiSelectInput(ChoiceInput): |
1375 | +class MultiSelectInput(MultiChoiceInputMixin, ChoiceInput): |
1376 | """ |
1377 | Multiple-selection list box input. |
1378 | """ |
1379 | @@ -925,7 +1021,11 @@ |
1380 | class ObjectMultiSelectInput(ObjectMultiChoiceMixin, MultiSelectInput): |
1381 | """ |
1382 | Variant of L{MultiSelectInput} for arbitrary Python objects. |
1383 | + |
1384 | + Deprecated. Use L{MultiSelectInput} with L{methanal.enums.ObjectEnum}. |
1385 | """ |
1386 | +ObjectMultiSelectInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1387 | + ObjectMultiSelectInput.__init__) |
1388 | |
1389 | |
1390 | |
1391 | @@ -938,27 +1038,23 @@ |
1392 | (u'Group name', [(u'value', u'Description'), |
1393 | ...]), |
1394 | ...) |
1395 | + |
1396 | + Deprecated. Use L{methanal.view.SelectInput} with L{methanal.enums.Enum} |
1397 | + values with a C{'group'} extra value instead. |
1398 | """ |
1399 | - @renderer |
1400 | - def options(self, req, tag): |
1401 | - option = tag.patternGenerator('option') |
1402 | - optgroup = tag.patternGenerator('optgroup') |
1403 | - |
1404 | - for groupName, values in self.values.asPairs(): |
1405 | - g = optgroup().fillSlots('label', groupName) |
1406 | - for value, description in values: |
1407 | - o = option() |
1408 | - o.fillSlots('value', value) |
1409 | - o.fillSlots('description', description) |
1410 | - g[o] |
1411 | - yield g |
1412 | +GroupedSelectInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1413 | + GroupedSelectInput.__init__) |
1414 | |
1415 | |
1416 | |
1417 | class ObjectGroupedSelectInput(ObjectChoiceMixin, SelectInput): |
1418 | """ |
1419 | Variant of L{GroupedSelectInput} for arbitrary Python objects. |
1420 | + |
1421 | + Deprecated. Use L{SelectInput} with L{methanal.enums.ObjectEnum}. |
1422 | """ |
1423 | +ObjectGroupedSelectInput.__init__ = deprecated(Version('methanal', 0, 2, 1))( |
1424 | + ObjectGroupedSelectInput.__init__) |
1425 | |
1426 | |
1427 |
Looking at some of this code now a few things jump out at me, I'd appreciate some thoughts on these:
1. "IEnumeration. asPairs" is basically only used in the tests now, do we actually need this method to be in IEnumeration?
2. "IEnumeration" only specifies a single method, as I've already found out, that is really not all that useful. I think the interface should be expanded.
3. Iterating an Enum yields non-hidden EnumItems, should there be some way to get all EnumItems? I don't really know why you would need this, but if you do it will probably be nicer than relying on a private attribute.