Merge lp:~methanal-developers/methanal/enum-object-and-group-choices into lp:methanal

Proposed by Jonathan Jacobs
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
Reviewer Review Type Date Requested Status
Tristan Seligmann Approve
Review via email: mp+28684@code.launchpad.net

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.

To post a comment you must log in.
145. By Jonathan Jacobs

Make Enum.__iter__ respect EnumItem.hidden.

146. By Jonathan Jacobs

Name tweaks.

Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

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.

147. By Jonathan Jacobs

Cosmetic tweaks.

Revision history for this message
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(theList[0][1]), (tuple, list))". Also, can we deprecate ListEnumeration? It's really awful.

1179 +ObjectRadioGroupInput.__init__ = deprecated(Version('methanal', 0, 2, 1))(
1180 + ObjectRadioGroupInput.__init__)

Can't this be done using @decorator syntax instead? I think it might be clearer that way.

review: Needs Fixing
148. By Jonathan Jacobs

Move more of Enum into IEnumeration.

149. By Jonathan Jacobs

Improve Enum.__repr__.

150. By Jonathan Jacobs

Deprecate ListEnumeration.

Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

> Regarding the rest of the branch:
>
> 34 + type(theList[0][1]) not in (tuple, list)):
>
> Rather use "not isinstance(theList[0][1]), (tuple, list))". Also, can we
> 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.

Revision history for this message
Tristan Seligmann (mithrandi) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches