Coverage for tw2/core/widgets.py : 99%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import absolute_import
3import copy
4import weakref
5import re
6import itertools
7import inspect
8import webob
9import uuid
11from . import templating
12from . import core
13from . import util
14from . import validation as vd
15from . import params as pm
16import six
17from six.moves import filter
18from markupsafe import Markup
20try:
21 import formencode
22except ImportError:
23 formencode = None
25reserved_names = (
26 'parent',
27 'demo_for',
28 'child',
29 'submit',
30 'datasrc',
31 'newlink',
32 'edit',
33)
34_widget_seq = itertools.count(0)
35_omitted = object()
38class WidgetMeta(pm.ParamMeta):
39 """
40 This metaclass:
42 * Detects members that are widgets, and constructs the
43 `children` parameter based on this.
44 * Gives widgets a sequence number, so ordering works correctly.
45 * Calls post_define for the widget class and base classes. This
46 is needed as it's not possible to call super() in post_define.
47 """
48 @classmethod
49 def _collect_base_children(meta, bases):
50 ''' Collect the children from the base classes '''
51 children = []
52 for b in bases:
53 bcld = getattr(b, 'children', None)
54 if bcld and not isinstance(bcld, RepeatingWidgetBunchCls):
55 children.extend(bcld)
56 return children
58 def __new__(meta, name, bases, dct):
59 if name != 'Widget' and 'children' not in dct:
60 # Children not provided,
61 # build them from class attributes.
62 new_children = []
63 for d, v in list(dct.items()):
64 if isinstance(v, type) and \
65 issubclass(v, Widget) and \
66 d not in reserved_names:
68 new_children.append((v, d))
69 del dct[d]
71 base_children = meta._collect_base_children(bases)
72 new_children = sorted(new_children, key=lambda t: t[0]._seq)
73 direct_children = [
74 hasattr(v, 'id') and v or v(id=d) for v, d in new_children
75 ]
76 direct_children_ids = set([c.id for c in direct_children])
77 dct['children'] = [
78 # Do not include children that have been overwritten in a subclass.
79 c for c in base_children if getattr(c, "id", _omitted) not in direct_children_ids
80 ] + direct_children
82 widget = super(WidgetMeta, meta).__new__(meta, name, bases, dct)
84 widget._seq = six.advance_iterator(_widget_seq)
85 for w in reversed(widget.__mro__):
86 if 'post_define' in w.__dict__:
87 w.post_define.__func__(widget)
88 return widget
91class Widget(six.with_metaclass(WidgetMeta, pm.Parametered)):
92 """
93 Base class for all widgets.
94 """
96 id = pm.Param('Widget identifier', request_local=False)
97 key = pm.Param('Widget data key; None just uses id',
98 default=None, request_local=False)
99 template = pm.Param(
100 'Template file for the widget, in the format ' +
101 'engine_name:template_path. If `engine_name` is specified, this ' +
102 'is interepreted not as a path, but as an *inline template*',
103 )
104 inline_engine_name = pm.Param(
105 'Name of an engine. If specified, `template` is interpreted as ' +
106 'an *inline template* and not a path.',
107 default=None,
108 )
109 validator = pm.Param(
110 'Validator for the widget.',
111 default=None,
112 request_local=False,
113 )
114 attrs = pm.Param(
115 "Extra attributes to include in the widget's outer-most HTML tag.",
116 default={},
117 )
118 css_class = pm.Param(
119 'CSS class name',
120 default=None,
121 attribute=True,
122 view_name='class',
123 )
124 value = pm.Param("The value for the widget.", default=None)
125 resources = pm.Param(
126 "Resources used by the widget. This must be an iterable, each " + \
127 "item of which is a :class:`Resource` subclass.",
128 default=[],
129 request_local=False,
130 )
132 error_msg = pm.Variable("Validation error message.")
133 parent = pm.Variable(
134 "The parent of this widget, or None if this is a root widget."
135 )
137 _sub_compound = False
138 _valid_id_re = re.compile(r'^[a-zA-Z][\w\-\_\.]*$')
140 @classmethod
141 def req(cls, **kw):
142 """
143 Generate an instance of the widget.
145 Return the validated widget for this request if one exists.
146 """
148 ins = None
150 # Create an instance. First check for the validated widget.
151 vw = vw_class = core.request_local().get('validated_widget')
152 if vw:
153 # Pull out actual class instances to compare to see if this
154 # is really the widget that was actually validated
155 if not getattr(vw_class, '__bases__', None):
156 vw_class = vw.__class__
158 if vw_class is not cls:
159 vw = None
161 if vw:
162 ins = vw
163 for key, value in kw.items():
164 setattr(ins, key, value)
166 if ins is None:
167 # We weren't the validated widget (or there wasn't one), so
168 # create a new instance
169 ins = object.__new__(cls)
170 ins.__init__(**kw)
172 return ins
174 def __new__(cls, id=None, **kw):
175 """
176 New is overloaded to return a subclass of the widget, rather than an
177 instance.
178 """
180 # Support backwards compatibility with tw1-style calling
181 if id and 'id' not in kw:
182 kw['id'] = id
184 newname = calc_name(cls, kw)
185 return type(cls.__name__ + '_s', (cls, ), kw)
187 def __init__(self, **kw):
188 for k, v in six.iteritems(kw):
189 setattr(self, k, v)
190 self._js_calls = []
192 @classmethod
193 def post_define(cls):
194 """
195 This is a class method, that is called when a subclass of this Widget
196 is created. Process static configuration here. Use it like this::
198 class MyWidget(LeafWidget):
199 @classmethod
200 def post_define(cls):
201 id = getattr(cls, 'id', None)
202 if id and not id.startswith('my'):
203 raise pm.ParameterError("id must start with 'my'")
205 post_define should always cope with missing data - the class may be an
206 abstract class. There is no need to call super(), the metaclass will do
207 this automatically.
208 """
210 if getattr(cls, 'id', None):
211 if not cls._valid_id_re.match(cls.id):
212 # http://www.w3schools.com/tags/att_standard_id.asp
213 raise pm.ParameterError(
214 "Not a valid W3C id: '%s'" % cls.id)
216 if hasattr(cls, 'id') and not getattr(cls, 'key', None):
217 cls.key = cls.id
219 cls.compound_id = cls._gen_compound_id(for_url=False)
220 if cls.compound_id:
221 cls.attrs = cls.attrs.copy()
222 cls.attrs['id'] = cls.compound_id
224 cls.compound_key = cls._gen_compound_key()
226 if hasattr(cls, 'request') and getattr(cls, 'id', None):
227 from . import middleware
228 path = cls._gen_compound_id(for_url=True)
229 middleware.register_controller(cls, path)
231 if cls.validator:
232 if cls.validator is pm.Required:
233 vld = cls.__mro__[1].validator
234 cls.validator = vld and vld.clone(required=True) or \
235 vd.Validator(required=True)
237 if isinstance(cls.validator, type) and \
238 issubclass(cls.validator, vd.Validator):
239 cls.validator = cls.validator()
241 if formencode and isinstance(cls.validator, type) and \
242 issubclass(cls.validator, formencode.Validator):
243 cls.validator = cls.validator()
245 if not isinstance(cls.validator, vd.Validator) and \
246 not (formencode and
247 isinstance(cls.validator, formencode.Validator)):
248 raise pm.ParameterError(
249 "Validator must be either a tw2 or FormEncode validator"
250 )
252 cls.resources = [r(parent=cls) for r in cls.resources]
253 cls._deferred = [k for k, v in inspect.getmembers(cls)
254 if isinstance(v, pm.Deferred)]
255 cls._attr = [p.name for p in cls._params.values() if p.attribute]
257 if cls.parent:
258 for p in cls.parent._all_params.values():
259 if p.child_param and \
260 not hasattr(cls, p.name) and \
261 p.default is not pm.Required:
263 setattr(cls, p.name, p.default)
265 @classmethod
266 def _gen_compound_name(cls, attr, for_url):
267 ancestors = []
268 cur = cls
269 while cur:
270 if cur in ancestors:
271 raise core.WidgetError('Parent loop')
272 ancestors.append(cur)
273 cur = cur.parent
274 elems = reversed(list(filter(None, [
275 a._compound_name_elem(attr, for_url) for a in ancestors
276 ])))
277 if getattr(cls, attr, None) or \
278 (cls.parent and issubclass(cls.parent, RepeatingWidget)):
279 return ':'.join(elems)
280 else:
281 return None
283 @classmethod
284 def _compound_name_elem(cls, attr, for_url):
285 if cls.parent and issubclass(cls.parent, RepeatingWidget):
286 if for_url:
287 return None
288 else:
289 return str(getattr(cls, 'repetition', None))
290 else:
291 return getattr(cls, attr, None)
293 @classmethod
294 def _compound_id_elem(cls, for_url):
295 return cls._compound_name_elem('id', for_url)
297 @classmethod
298 def _gen_compound_id(cls, for_url):
299 return cls._gen_compound_name('id', for_url)
301 @classmethod
302 def _gen_compound_key(cls):
303 return cls._gen_compound_name('key', False)
305 @classmethod
306 def get_link(cls):
307 """
308 Get the URL to the controller . This is called at run time, not startup
309 time, so we know the middleware if configured with the controller path.
310 Note: this function is a temporary measure, a cleaner API for this is
311 planned.
312 """
313 if not hasattr(cls, 'request') or not getattr(cls, 'id', None):
314 raise core.WidgetError('Not a controller widget')
315 mw = core.request_local()['middleware']
316 return mw.config.controller_prefix + cls._gen_compound_id(for_url=True)
318 def prepare(self):
319 """
320 This is an instance method, that is called just before the Widget is
321 displayed. Process request-local configuration here. For
322 efficiency, widgets should do as little work as possible here.
323 Use it like this::
325 class MyWidget(Widget):
326 def prepare(self):
327 super(MyWidget, self).prepare()
328 self.value = 'My: ' + str(self.value)
329 """
331 # First, if we don't already have an id, then pick a random one.
332 if not hasattr(self, 'id'):
333 self.id = 'id_' + str(uuid.uuid4()).replace('-', '')
335 # Then, enforce any params marked with twc.Required.
336 for k, v in self._params.items():
337 if v.default is pm.Required and not hasattr(self, k):
338 raise ValueError(
339 "%r is a required Parameter for %r" % (k, self))
341 for a in self._deferred:
342 dfr = getattr(self, a)
343 if isinstance(dfr, pm.Deferred):
344 setattr(self, a, dfr.fn())
346 if self.validator and not hasattr(self, '_validated'):
347 value = self.value
349 # Handles the case where FE expects dict-like object, but
350 # you have None at your disposal.
351 if formencode and \
352 isinstance(self.validator, formencode.Validator) and \
353 self.value is None:
354 value = {}
356 try:
357 value = self.validator.from_python(value)
358 except vd.catch as e:
359 value = str(value)
360 self.error_msg = e.msg
362 if formencode and value == {} and self.value is None:
363 value = None
365 self.value = value
367 if self._attr or 'attrs' in self.__dict__:
368 self.attrs = self.attrs.copy()
369 if self.compound_id:
370 self.attrs['id'] = self.compound_id
372 for a in self._attr:
373 view_name = self._params[a].view_name
374 if self.attrs.get(view_name):
375 raise pm.ParameterError(
376 "Attr param clashes with user-supplied attr: '%s'" % a
377 )
378 self.attrs[view_name] = getattr(self, a)
380 def iteritems(self):
381 """
382 An iterator which will provide the params of the widget in
383 key, value pairs.
384 """
385 for param in self._params.keys():
386 value = getattr(self, param)
387 yield param, value
389 @util.class_or_instance
390 def controller_path(self, cls):
391 """ Return the URL path against which this widget's controller is
392 mounted or None if it is not registered with the ControllerApp.
393 """
395 mw = core.request_local().get('middleware')
396 return mw.controllers.controller_path(cls)
398 @util.class_or_instance
399 def add_call(self, extra_arg, call, location="bodybottom"):
400 """
401 Not sure what the "extra_arg" needed is for, but it is needed, as is
402 the decorator, or an infinite loop ensues.
404 Adds a :func:`tw.api.js_function` call that will be made when the
405 widget is rendered.
406 """
407 #log.debug("Adding call <%s> for %r statically.", call, self)
408 self._js_calls.append([call, location])
410 @util.class_or_instance
411 def display(self, cls, value=None, displays_on=None, **kw):
412 """Display the widget - render the template. In the template, the
413 widget instance is available as the variable ``$w``.
415 If display is called on a class, it automatically creates an instance.
417 `displays_on`
418 The name of the template engine this widget is being displayed
419 inside. If not specified, this is determined automatically, from
420 the parent's template engine, or the default, if there is no
421 parent. Set this to ``string`` to get raw string output.
422 """
424 # Support backwards compatibility with tw1-style calling
425 if value is not None and 'value' not in kw:
426 kw['value'] = value
428 # Support arguments to .display on either instance or class
429 # https://github.com/toscawidgets/tw2.core/issues/41
430 if self:
431 for key, value in kw.items():
432 setattr(self, key, value)
433 else:
434 self = cls.req(**kw)
436 # Register any deferred params that are handed to us late in the game
437 # (after post_define). The .prepare method handles processing them
438 # later.
439 self._deferred += [k for k, v in kw.items() if isinstance(v, pm.Deferred)]
441 if not self.parent:
442 self.prepare()
444 if self._js_calls:
445 self.safe_modify('resources')
446 #avoids circular reference
447 from . import resources as rs
448 for item in self._js_calls:
449 if 'JSFuncCall' in repr(item[0]):
450 self.resources.append(item[0])
451 else:
452 self.resources.append(rs._JSFuncCall(
453 src=str(item[0]),
454 location=item[1],
455 ))
457 if self.resources:
458 self.resources = WidgetBunch([r.req() for r in self.resources])
459 for r in self.resources:
460 r.prepare()
462 return self.generate_output(displays_on)
464 def generate_output(self, displays_on):
465 """
466 Generate the actual output text for this widget.
468 By default this renders the widget's template. Subclasses can override
469 this method for purely programmatic output.
471 `displays_on`
472 The name of the template engine this widget is being displayed
473 inside.
475 Use it like this::
477 class MyWidget(LeafWidget):
478 def generate_output(self, displays_on):
479 return "<span {0}>{1}</span>".format(self.attrs, self.text)
480 """
482 mw = core.request_local().get('middleware')
484 if not displays_on:
485 displays_on = self._get_default_displays_on(mw)
487 # Build the arguments used while rendering the template
488 kwargs = {'w': self}
489 if mw and mw.config.params_as_vars:
490 for p in self._params:
491 if hasattr(self, p):
492 kwargs[p] = getattr(self, p)
494 if self.template is None:
495 raise ValueError("A template must be provided.")
497 return templating.render(
498 self.template,
499 displays_on,
500 kwargs,
501 self.inline_engine_name,
502 mw,
503 )
505 def _get_default_displays_on(self, mw):
506 if not self.parent:
507 if mw:
508 return mw.config.default_engine
509 return 'string'
510 else:
511 return templating.get_engine_name(self.parent.template, mw)
513 @classmethod
514 def validate(cls, params, state=None):
515 """
516 Validate form input. This should always be called on a class. It
517 either returns the validated data, or raises a
518 :class:`ValidationError` exception.
519 """
520 if cls.parent:
521 raise core.WidgetError('Only call validate on root widgets')
522 value = vd.unflatten_params(params)
523 if hasattr(cls, 'id') and cls.id:
524 value = value.get(cls.id, {})
525 ins = cls.req()
527 # Key the validated widget by class id
528 core.request_local()['validated_widget'] = ins
529 return ins._validate(value, state)
531 @vd.catch_errors
532 def _validate(self, value, state=None):
533 """
534 Inner validation method; this is called by validate and should not be
535 called directly. Overriding this method in widgets is discouraged; a
536 custom validator should be coded instead. However, in some
537 circumstances overriding is necessary.
538 """
539 self._validated = True
540 self.value = value
541 if self.validator:
542 value = self.validator.to_python(value, state)
543 return value
545 def safe_modify(self, attr):
546 if (attr not in self.__dict__ and
547 isinstance(getattr(self, attr, None), (dict, list))):
548 setattr(self, attr, copy.copy(getattr(self, attr)))
550 @classmethod
551 def children_deep(cls):
552 yield cls
555class LeafWidget(Widget):
556 """
557 A widget that has no children; this is the most common kind, e.g. form
558 fields.
559 """
562class WidgetBunch(list):
563 def __getattr__(self, id):
564 for w in self:
565 if w.id == id:
566 return w
567 raise AttributeError("Widget has no child named '%s'" % id)
570class CompoundWidget(Widget):
571 """
572 A widget that has an arbitrary number of children, this is common for
573 layout components, such as :class:`tw2.forms.TableLayout`.
574 """
575 children = pm.Param(
576 'Children for this widget. This must be an iterable, ' +
577 'each item of which is a Widget'
578 )
579 c = pm.Variable(
580 "Alias for children",
581 default=property(lambda s: s.children),
582 )
583 children_deep = pm.Variable(
584 "Children, including any children from child " +
585 "CompoundWidgets that have no id",
586 )
587 template = 'tw2.core.templates.display_children'
588 separator = pm.Param('HTML snippet which will be inserted '
589 'between each repeated child', default=None)
591 @classmethod
592 def post_define(cls):
593 """
594 Check children are valid; update them to have a link to the parent.
595 """
596 cls._sub_compound = not getattr(cls, 'id', None)
597 if not hasattr(cls, 'children'):
598 return
600 joined_cld = []
601 for c in cls.children:
602 if not isinstance(c, type) or not issubclass(c, Widget):
603 raise pm.ParameterError("All children must be widgets")
604 joined_cld.append(c(parent=cls))
606 ids = set()
607 for c in cls.children_deep():
608 if getattr(c, 'id', None):
609 if c.id in ids:
610 raise core.WidgetError("Duplicate id '%s'" % c.id)
611 ids.add(c.id)
613 cls.children = WidgetBunch(joined_cld)
614 cls.keyed_children = [
615 c.id for c in joined_cld
616 if hasattr(c, 'key') and hasattr(c, 'id') and c.key != c.id
617 ]
619 def __init__(self, **kw):
620 super(CompoundWidget, self).__init__(**kw)
621 self.children = WidgetBunch(
622 c.req(parent=weakref.proxy(self))
623 for c in self.children
624 )
626 def prepare(self):
627 """
628 Propagate the value for this widget to the children, based on their id.
629 """
630 super(CompoundWidget, self).prepare()
631 if self.separator:
632 self.separator = Markup(self.separator)
633 v = self.value or {}
634 if not hasattr(self, '_validated'):
635 if hasattr(v, '__getitem__'):
636 for c in self.children:
637 if c._sub_compound:
638 c.value = v
639 elif c.key in v:
640 c.value = v[c.key]
641 else:
642 for c in self.children:
643 if c._sub_compound:
644 c.value = self.value
645 else:
646 c.value = getattr(self.value, c.key or '', None)
647 for c in self.children:
648 c.prepare()
650 def get_child_error_message(self, name):
651 if isinstance(self.error_msg, six.string_types):
652 if self.error_msg.startswith(name + ':'):
653 return self.error_msg.split(':')[1]
655 @vd.catch_errors
656 def _validate(self, value, state=None):
657 """
658 The value must be a dict, or None. Each item in the dict is passed to
659 the corresponding child widget for validation, with special
660 consideration for _sub_compound widgets. If a child returns
661 vd.EmptyField, that value is not included in the resulting dict at all,
662 which is different to including None. Child widgets with a key are
663 passed the validated value from the field the key references. The
664 resulting dict is validated by this widget's validator. If any child
665 widgets produce an errors, this results in a "childerror" failure.
666 """
667 self._validated = True
668 value = value or {}
669 if not isinstance(value, dict):
670 raise vd.ValidationError('corrupt', self.validator)
671 self.value = value
672 any_errors = False
673 data = {}
675 state = util.clone_object(state, full_dict=value, validated_values=data)
677 # Validate compound children
678 for c in (child for child in self.children if child._sub_compound):
679 try:
680 data.update(c._validate(value, state))
681 except vd.catch as e:
682 if hasattr(e, 'msg'):
683 c.error_msg = e.msg
684 any_errors = True
686 # Validate non compound children
687 for c in (child for child in self.children if not child._sub_compound):
688 d = value.get(c.key, '')
689 try:
690 val = c._validate(d, state)
691 if val is not vd.EmptyField:
692 data[c.key] = val
693 except vd.catch as e:
694 if hasattr(e, 'msg'):
695 c.error_msg = e.msg
696 data[c.key] = vd.Invalid
697 any_errors = True
699 # Validate self, usually a CompoundValidator or a FormEncode form-level
700 # validator.
701 exception_validator = self.validator
702 if self.validator:
703 try:
704 data = self.validator.to_python(data, state)
705 except vd.catch as e:
706 # If it failed to validate, check if the error_dict has any
707 # messages pertaining specifically to this widget's children.
708 error_dict = getattr(e, 'error_dict', {})
709 if not error_dict:
710 raise
712 for c in self.children:
713 if getattr(c, 'key', None) in error_dict:
714 c.error_msg = error_dict[c.key]
715 data[c.key] = vd.Invalid
716 exception_validator = None
717 any_errors = True
719 # Only re-raise this top-level exception if the validation
720 # error doesn't pertain to any of our children.
721 if exception_validator:
722 raise
724 if any_errors:
725 raise vd.ValidationError('childerror', exception_validator)
727 return data
729 @classmethod
730 def children_deep(cls):
731 if getattr(cls, 'id', None):
732 yield cls
733 else:
734 for c in getattr(cls, 'children', []):
735 for cc in c.children_deep():
736 yield cc
739class RepeatingWidgetBunchCls(object):
741 def __init__(self, parent):
742 self.parent = parent
743 self._repetition_cache = {}
745 def __getitem__(self, item):
746 if not isinstance(item, int):
747 raise KeyError("Must specify an integer")
748 try:
749 rep = self._repetition_cache[item]
750 except KeyError:
751 rep = self.parent.child(parent=self.parent, repetition=item)
752 self._repetition_cache[item] = rep
753 return rep
756class RepeatingWidgetBunch(object):
757 def __init__(self, parent, rwbc):
758 self.parent = parent
759 self.rwbc = rwbc
760 self._repetition_cache = {}
762 def __len__(self):
763 return self.parent.repetitions
765 def __iter__(self):
766 for i in range(len(self)):
767 yield self[i]
769 def __getitem__(self, item):
770 if not isinstance(item, int):
771 raise KeyError("Must specify an integer")
772 try:
773 rep = self._repetition_cache[item]
774 except KeyError:
775 rep = self.rwbc[item].req(parent=weakref.proxy(self.parent))
776 self._repetition_cache[item] = rep
777 return rep
780class RepeatingWidget(Widget):
781 """
782 A widget that has a single child, which is repeated an arbitrary number
783 of times, such as :class:`tw2.forms.GridLayout`.
784 """
785 child = pm.Param('Child for this widget. The child must have no id.')
786 repetitions = pm.Param(
787 'Fixed number of repetitions. If this is None, it dynamically ' +
788 'determined, based on the length of the value list.',
789 default=None,
790 )
791 min_reps = pm.Param('Minimum number of repetitions', default=None)
792 max_reps = pm.Param('Maximum number of repetitions', default=None)
793 extra_reps = pm.Param(
794 'Number of extra repeitions, beyond the length of the value list.',
795 default=0,
796 )
797 children = pm.Param(
798 'Children specified for this widget will be passed to the child. ' +
799 'In the template, children gets the list of repeated childen.',
800 default=[],
801 )
803 repetition = pm.ChildVariable('The repetition of a child widget.')
805 template = 'tw2.core.templates.display_children'
806 separator = pm.Param('HTML snippet which will be inserted '
807 'between each repeated child', default=None)
809 @classmethod
810 def post_define(cls):
811 """
812 Check child is valid; update with link to parent.
813 """
814 if not hasattr(cls, 'child'):
815 return
817 if getattr(cls, 'children', None):
818 cls.child = cls.child(children=cls.children)
819 cls.children = []
821 if not isinstance(cls.child, type) or \
822 not issubclass(cls.child, Widget):
823 raise pm.ParameterError("Child must be a Widget")
825 if issubclass(cls.child, DisplayOnlyWidget):
826 raise pm.ParameterError('Child cannot be a DisplayOnlyWidget')
828 if getattr(cls.child, 'id', None):
829 raise pm.ParameterError("Child must have no id")
831 cls.child = cls.child(parent=cls)
832 cls.rwbc = RepeatingWidgetBunchCls(parent=cls)
834 def __init__(self, **kw):
835 super(RepeatingWidget, self).__init__(**kw)
836 self.children = RepeatingWidgetBunch(self, self.rwbc)
838 def prepare(self):
839 """
840 Propagate the value for this widget to the children, based on their
841 index.
842 """
843 super(RepeatingWidget, self).prepare()
844 if self.separator:
845 self.separator = Markup(self.separator)
846 value = self.value or []
847 if self.repetitions is None:
848 reps = len(value) + self.extra_reps
849 if self.max_reps is not None and reps > self.max_reps:
850 reps = self.max_reps
851 if self.min_reps is not None and reps < self.min_reps:
852 reps = self.min_reps
853 self.repetitions = reps
855 for i, v in enumerate(value):
856 self.children[i].value = v
857 for c in self.children:
858 c.prepare()
859 if not self.repetitions:
860 self.children[0].prepare()
862 @vd.catch_errors
863 def _validate(self, value, state=None):
864 """
865 The value must either be a list or None. Each item in the list is
866 passed to the corresponding child widget for validation. The resulting
867 list is passed to this widget's validator. If any of the child widgets
868 produces a validation error, this widget generates a "childerror"
869 failure.
870 """
871 self._validated = True
872 value = value or []
873 if not isinstance(value, list):
874 raise vd.ValidationError('corrupt', self.validator, self)
875 self.value = value
876 any_errors = False
877 data = []
879 state = util.clone_object(state, full_dict=value, validated_values=data)
881 for i, v in enumerate(value):
882 try:
883 data.append(self.children[i]._validate(v, state))
884 except vd.catch:
885 data.append(vd.Invalid)
886 any_errors = True
887 if self.validator:
888 data = self.validator.to_python(data, state)
889 if any_errors:
890 raise vd.ValidationError('childerror', self.validator, self)
891 return data
894class DisplayOnlyWidgetMeta(WidgetMeta):
895 @classmethod
896 def _collect_base_children(meta, bases):
897 children = []
898 for b in bases:
899 bchild = getattr(b, 'child', None)
900 if bchild:
901 b = b.child
902 bcld = getattr(b, 'children', None)
903 if bcld and not isinstance(bcld, RepeatingWidgetBunchCls):
904 children.extend(bcld)
905 return children
908def calc_name(cls, kw, char='s'):
909 if 'parent' in kw:
910 newname = kw['parent'].__name__ + '__' + cls.__name__
911 else:
912 newname = cls.__name__ + '_%s' % char
913 return newname
916class DisplayOnlyWidget(six.with_metaclass(DisplayOnlyWidgetMeta, Widget)):
917 """
918 A widget that has a single child. The parent widget is only used for
919 display purposes; it does not affect value propagation or validation.
920 This is used by widgets like :class:`tw2.forms.FieldSet`.
921 """
922 child = pm.Param('Child for this widget.')
923 children = pm.Param(
924 'Children specified for this widget will be passed to the child',
925 default=[],
926 )
927 id_suffix = pm.Variable('Suffix to append to compound IDs', default=None)
929 def __new__(cls, **kw):
930 newname = calc_name(cls, kw, 'd')
931 return type(newname, (cls,), kw)
933 @classmethod
934 def post_define(cls):
935 if not getattr(cls, 'child', None):
936 return
938 if getattr(cls, 'children', None):
939 cls.child = cls.child(children=cls.children)
940 cls.children = []
942 if getattr(cls, 'validator', None):
943 cls.child.validator = cls.validator
944 cls.validator = None
946 if not isinstance(cls.child, type) or \
947 not issubclass(cls.child, Widget):
948 raise pm.ParameterError("Child must be a widget")
950 cls.compound_key = None
951 cls._sub_compound = cls.child._sub_compound
952 cls_id = getattr(cls, 'id', None)
953 child_id = getattr(cls.child, 'id', None)
954 if cls_id and child_id and cls_id != child_id:
955 raise pm.ParameterError(
956 "Can only specify id on either a DisplayOnlyWidget, or " +
957 "its child, not both: '%s' '%s'" % (cls_id, child_id)
958 )
959 if not cls_id and child_id:
960 cls.id = child_id
961 DisplayOnlyWidget.post_define.__func__(cls)
962 Widget.post_define.__func__(cls)
963 cls.child = cls.child(parent=cls, key=cls.key)
964 else:
965 cls.child = cls.child(id=cls_id, key=cls.key, parent=cls)
967 @classmethod
968 def _gen_compound_name(cls, attr, for_url):
969 elems = [
970 Widget._gen_compound_name.__func__(cls, attr, for_url),
971 getattr(cls, attr, None)
972 ]
973 elems = list(filter(None, elems))
974 if not elems:
975 return None
976 if not for_url and attr=='id' and getattr(cls, 'id_suffix', None):
977 elems.append(cls.id_suffix)
978 return ':'.join(elems)
980 @classmethod
981 def _compound_name_elem(cls, attr, for_url):
982 if cls.parent and issubclass(cls.parent, RepeatingWidget):
983 Widget._compound_name_elem.__func__(cls, attr, for_url)
984 else:
985 return None
987 def __init__(self, **kw):
988 super(DisplayOnlyWidget, self).__init__(**kw)
989 if hasattr(self, 'child'):
990 self.child = self.child.req(parent=weakref.proxy(self))
991 else:
992 self.child = None
994 def prepare(self):
995 super(DisplayOnlyWidget, self).prepare()
996 if self.child:
997 if not hasattr(self, '_validated'):
998 self.child.value = self.value
999 self.child.prepare()
1001 @vd.catch_errors
1002 def _validate(self, value, state=None):
1003 self._validated = True
1004 try:
1005 return self.child._validate(value, state)
1006 except vd.ValidationError:
1007 raise vd.ValidationError('childerror', self.validator, self)
1009 @classmethod
1010 def children_deep(cls):
1011 for c in cls.child.children_deep():
1012 yield c
1015def default_content_type():
1016 "default_content_type"
1017 return "text/html; charset=%s" % (
1018 core.request_local()['middleware'].config.encoding
1019 )
1022class Page(DisplayOnlyWidget):
1023 """
1024 An HTML page. This widget includes a :meth:`request` method that serves
1025 the page.
1026 """
1027 title = pm.Param('Title for the page', default=None)
1028 content_type = pm.Param(
1029 'Content type header',
1030 default=pm.Deferred(default_content_type),
1031 request_local=False,
1032 )
1033 template = "tw2.core.templates.page"
1034 id_suffix = 'page'
1035 _no_autoid = True
1037 @classmethod
1038 def post_define(cls):
1039 if not getattr(cls, 'id', None) and '_no_autoid' not in cls.__dict__:
1040 cls.id = cls.__name__.lower()
1041 DisplayOnlyWidget.post_define.__func__(cls)
1042 Widget.post_define.__func__(cls)
1044 @classmethod
1045 def request(cls, req):
1046 ct = cls.content_type
1047 if isinstance(ct, pm.Deferred):
1048 ct = ct.fn()
1049 resp = webob.Response(request=req, content_type=ct)
1050 ins = cls.req()
1051 ins.fetch_data(req)
1052 resp.body = ins.display().encode(
1053 core.request_local()['middleware'].config.encoding
1054 )
1055 return resp
1057 def fetch_data(self, req):
1058 pass