Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/forms/base.py: 100%
280 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 22:41 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 22:41 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Base form classes
25"""
27import logging
28from collections import OrderedDict
30import colander
31import deform
32from colanderalchemy import SQLAlchemySchemaNode
33from pyramid.renderers import render
34from webhelpers2.html import HTML
36from wuttaweb.util import FieldList, get_form_data, get_model_fields, make_json_safe
39log = logging.getLogger(__name__)
42class Form:
43 """
44 Base class for all forms.
46 :param request: Reference to current :term:`request` object.
48 :param fields: List of field names for the form. This is
49 optional; if not specified an attempt will be made to deduce
50 the list automatically. See also :attr:`fields`.
52 :param schema: Colander-based schema object for the form. This is
53 optional; if not specified an attempt will be made to construct
54 one automatically. See also :meth:`get_schema()`.
56 :param labels: Optional dict of default field labels.
58 .. note::
60 Some parameters are not explicitly described above. However
61 their corresponding attributes are described below.
63 Form instances contain the following attributes:
65 .. attribute:: request
67 Reference to current :term:`request` object.
69 .. attribute:: fields
71 :class:`~wuttaweb.util.FieldList` instance containing string
72 field names for the form. By default, fields will appear in
73 the same order as they are in this list.
75 See also :meth:`set_fields()`.
77 .. attribute:: schema
79 :class:`colander:colander.Schema` object for the form. This is
80 optional; if not specified an attempt will be made to construct
81 one automatically.
83 See also :meth:`get_schema()`.
85 .. attribute:: model_class
87 Model class for the form, if applicable. When set, this is
88 usually a SQLAlchemy mapped class. This (or
89 :attr:`model_instance`) may be used instead of specifying the
90 :attr:`schema`.
92 .. attribute:: model_instance
94 Optional instance from which initial form data should be
95 obtained. In simple cases this might be a dict, or maybe an
96 instance of :attr:`model_class`.
98 Note that this also may be used instead of specifying the
99 :attr:`schema`, if the instance belongs to a class which is
100 SQLAlchemy-mapped. (In that case :attr:`model_class` can be
101 determined automatically.)
103 .. attribute:: nodes
105 Dict of node overrides, used to construct the form in
106 :meth:`get_schema()`.
108 See also :meth:`set_node()`.
110 .. attribute:: widgets
112 Dict of widget overrides, used to construct the form in
113 :meth:`get_schema()`.
115 See also :meth:`set_widget()`.
117 .. attribute:: validators
119 Dict of node validators, used to construct the form in
120 :meth:`get_schema()`.
122 See also :meth:`set_validator()`.
124 .. attribute:: defaults
126 Dict of default field values, used to construct the form in
127 :meth:`get_schema()`.
129 See also :meth:`set_default()`.
131 .. attribute:: readonly
133 Boolean indicating the form does not allow submit. In practice
134 this means there will not even be a ``<form>`` tag involved.
136 Default for this is ``False`` in which case the ``<form>`` tag
137 will exist and submit is allowed.
139 .. attribute:: readonly_fields
141 A :class:`~python:set` of field names which should be readonly.
142 Each will still be rendered but with static value text and no
143 widget.
145 This is only applicable if :attr:`readonly` is ``False``.
147 See also :meth:`set_readonly()` and :meth:`is_readonly()`.
149 .. attribute:: required_fields
151 A dict of "required" field flags. Keys are field names, and
152 values are boolean flags indicating whether the field is
153 required.
155 Depending on :attr:`schema`, some fields may be "(not)
156 required" by default. However ``required_fields`` keeps track
157 of any "overrides" per field.
159 See also :meth:`set_required()` and :meth:`is_required()`.
161 .. attribute:: action_url
163 String URL to which the form should be submitted, if applicable.
165 .. attribute:: cancel_url
167 String URL to which the Cancel button should "always" redirect,
168 if applicable.
170 Code should not access this directly, but instead call
171 :meth:`get_cancel_url()`.
173 .. attribute:: cancel_url_fallback
175 String URL to which the Cancel button should redirect, if
176 referrer cannot be determined from request.
178 Code should not access this directly, but instead call
179 :meth:`get_cancel_url()`.
181 .. attribute:: vue_tagname
183 String name for Vue component tag. By default this is
184 ``'wutta-form'``. See also :meth:`render_vue_tag()`.
186 See also :attr:`vue_component`.
188 .. attribute:: align_buttons_right
190 Flag indicating whether the buttons (submit, cancel etc.)
191 should be aligned to the right of the area below the form. If
192 not set, the buttons are left-aligned.
194 .. attribute:: auto_disable_submit
196 Flag indicating whether the submit button should be
197 auto-disabled, whenever the form is submitted.
199 .. attribute:: button_label_submit
201 String label for the form submit button. Default is ``"Save"``.
203 .. attribute:: button_icon_submit
205 String icon name for the form submit button. Default is ``'save'``.
207 .. attribute:: button_type_submit
209 Buefy type for the submit button. Default is ``'is-primary'``,
210 so for example:
212 .. code-block:: html
214 <b-button type="is-primary"
215 native-type="submit">
216 Save
217 </b-button>
219 See also the `Buefy docs
220 <https://buefy.org/documentation/button/#api-view>`_.
222 .. attribute:: show_button_reset
224 Flag indicating whether a Reset button should be shown.
225 Default is ``False``.
227 .. attribute:: show_button_cancel
229 Flag indicating whether a Cancel button should be shown.
230 Default is ``True``.
232 .. attribute:: button_label_cancel
234 String label for the form cancel button. Default is
235 ``"Cancel"``.
237 .. attribute:: auto_disable_cancel
239 Flag indicating whether the cancel button should be
240 auto-disabled, whenever the button is clicked. Default is
241 ``True``.
243 .. attribute:: validated
245 If the :meth:`validate()` method was called, and it succeeded,
246 this will be set to the validated data dict.
248 Note that in all other cases, this attribute may not exist.
249 """
251 def __init__(
252 self,
253 request,
254 fields=None,
255 schema=None,
256 model_class=None,
257 model_instance=None,
258 nodes={},
259 widgets={},
260 validators={},
261 defaults={},
262 readonly=False,
263 readonly_fields=[],
264 required_fields={},
265 labels={},
266 action_url=None,
267 cancel_url=None,
268 cancel_url_fallback=None,
269 vue_tagname='wutta-form',
270 align_buttons_right=False,
271 auto_disable_submit=True,
272 button_label_submit="Save",
273 button_icon_submit='save',
274 button_type_submit='is-primary',
275 show_button_reset=False,
276 show_button_cancel=True,
277 button_label_cancel="Cancel",
278 auto_disable_cancel=True,
279 ):
280 self.request = request
281 self.schema = schema
282 self.nodes = nodes or {}
283 self.widgets = widgets or {}
284 self.validators = validators or {}
285 self.defaults = defaults or {}
286 self.readonly = readonly
287 self.readonly_fields = set(readonly_fields or [])
288 self.required_fields = required_fields or {}
289 self.labels = labels or {}
290 self.action_url = action_url
291 self.cancel_url = cancel_url
292 self.cancel_url_fallback = cancel_url_fallback
293 self.vue_tagname = vue_tagname
294 self.align_buttons_right = align_buttons_right
295 self.auto_disable_submit = auto_disable_submit
296 self.button_label_submit = button_label_submit
297 self.button_icon_submit = button_icon_submit
298 self.button_type_submit = button_type_submit
299 self.show_button_reset = show_button_reset
300 self.show_button_cancel = show_button_cancel
301 self.button_label_cancel = button_label_cancel
302 self.auto_disable_cancel = auto_disable_cancel
304 self.config = self.request.wutta_config
305 self.app = self.config.get_app()
307 self.model_class = model_class
308 self.model_instance = model_instance
309 if self.model_instance and not self.model_class:
310 if type(self.model_instance) is not dict:
311 self.model_class = type(self.model_instance)
313 self.set_fields(fields or self.get_fields())
315 # nb. this tracks grid JSON data for inclusion in page template
316 self.grid_vue_context = OrderedDict()
318 def __contains__(self, name):
319 """
320 Custom logic for the ``in`` operator, to allow easily checking
321 if the form contains a given field::
323 myform = Form()
324 if 'somefield' in myform:
325 print("my form has some field")
326 """
327 return bool(self.fields and name in self.fields)
329 def __iter__(self):
330 """
331 Custom logic to allow iterating over form field names::
333 myform = Form(fields=['foo', 'bar'])
334 for fieldname in myform:
335 print(fieldname)
336 """
337 return iter(self.fields)
339 @property
340 def vue_component(self):
341 """
342 String name for the Vue component, e.g. ``'WuttaForm'``.
344 This is a generated value based on :attr:`vue_tagname`.
345 """
346 words = self.vue_tagname.split('-')
347 return ''.join([word.capitalize() for word in words])
349 def get_cancel_url(self):
350 """
351 Returns the URL for the Cancel button.
353 If :attr:`cancel_url` is set, its value is returned.
355 Or, if the referrer can be deduced from the request, that is
356 returned.
358 Or, if :attr:`cancel_url_fallback` is set, that value is
359 returned.
361 As a last resort the "default" URL from
362 :func:`~wuttaweb.subscribers.request.get_referrer()` is
363 returned.
364 """
365 # use "permanent" URL if set
366 if self.cancel_url:
367 return self.cancel_url
369 # nb. use fake default to avoid normal default logic;
370 # that way if we get something it's a real referrer
371 url = self.request.get_referrer(default='NOPE')
372 if url and url != 'NOPE':
373 return url
375 # use fallback URL if set
376 if self.cancel_url_fallback:
377 return self.cancel_url_fallback
379 # okay, home page then (or whatever is the default URL)
380 return self.request.get_referrer()
382 def set_fields(self, fields):
383 """
384 Explicitly set the list of form fields.
386 This will overwrite :attr:`fields` with a new
387 :class:`~wuttaweb.util.FieldList` instance.
389 :param fields: List of string field names.
390 """
391 self.fields = FieldList(fields)
393 def append(self, *keys):
394 """
395 Add some fields(s) to the form.
397 This is a convenience to allow adding multiple fields at
398 once::
400 form.append('first_field',
401 'second_field',
402 'third_field')
404 It will add each field to :attr:`fields`.
405 """
406 for key in keys:
407 if key not in self.fields:
408 self.fields.append(key)
410 def remove(self, *keys):
411 """
412 Remove some fields(s) from the form.
414 This is a convenience to allow removal of multiple fields at
415 once::
417 form.remove('first_field',
418 'second_field',
419 'third_field')
421 It will remove each field from :attr:`fields`.
422 """
423 for key in keys:
424 if key in self.fields:
425 self.fields.remove(key)
427 def set_node(self, key, nodeinfo, **kwargs):
428 """
429 Set/override the node for a field.
431 :param key: Name of field.
433 :param nodeinfo: Should be either a
434 :class:`colander:colander.SchemaNode` instance, or else a
435 :class:`colander:colander.SchemaType` instance.
437 If ``nodeinfo`` is a proper node instance, it will be used
438 as-is. Otherwise an
439 :class:`~wuttaweb.forms.schema.ObjectNode` instance will be
440 constructed using ``nodeinfo`` as the type (``typ``).
442 Node overrides are tracked via :attr:`nodes`.
443 """
444 from wuttaweb.forms.schema import ObjectNode
446 if isinstance(nodeinfo, colander.SchemaNode):
447 # assume nodeinfo is a complete node
448 node = nodeinfo
450 else: # assume nodeinfo is a schema type
451 kwargs.setdefault('name', key)
452 node = ObjectNode(nodeinfo, **kwargs)
454 self.nodes[key] = node
456 # must explicitly replace node, if we already have a schema
457 if self.schema:
458 self.schema[key] = node
460 def set_widget(self, key, widget):
461 """
462 Set/override the widget for a field.
464 :param key: Name of field.
466 :param widget: Instance of
467 :class:`deform:deform.widget.Widget`.
469 Widget overrides are tracked via :attr:`widgets`.
470 """
471 self.widgets[key] = widget
473 # update schema if necessary
474 if self.schema and key in self.schema:
475 self.schema[key].widget = widget
477 def set_validator(self, key, validator):
478 """
479 Set/override the validator for a field, or the form.
481 :param key: Name of field. This may also be ``None`` in which
482 case the validator will apply to the whole form instead of
483 a field.
485 :param validator: Callable which accepts ``(node, value)``
486 args. For instance::
488 def validate_foo(node, value):
489 if value == 42:
490 node.raise_invalid("42 is not allowed!")
492 form = Form(fields=['foo', 'bar'])
494 form.set_validator('foo', validate_foo)
496 Validator overrides are tracked via :attr:`validators`.
497 """
498 self.validators[key] = validator
500 # nb. must apply to existing schema if present
501 if self.schema and key in self.schema:
502 self.schema[key].validator = validator
504 def set_default(self, key, value):
505 """
506 Set/override the default value for a field.
508 :param key: Name of field.
510 :param validator: Default value for the field.
512 Default value overrides are tracked via :attr:`defaults`.
513 """
514 self.defaults[key] = value
516 def set_readonly(self, key, readonly=True):
517 """
518 Enable or disable the "readonly" flag for a given field.
520 When a field is marked readonly, it will be shown in the form
521 but there will be no editable widget. The field is skipped
522 over (not saved) when form is submitted.
524 See also :meth:`is_readonly()`; this is tracked via
525 :attr:`readonly_fields`.
527 :param key: String key (fieldname) for the field.
529 :param readonly: New readonly flag for the field.
530 """
531 if readonly:
532 self.readonly_fields.add(key)
533 else:
534 if key in self.readonly_fields:
535 self.readonly_fields.remove(key)
537 def is_readonly(self, key):
538 """
539 Returns boolean indicating if the given field is marked as
540 readonly.
542 See also :meth:`set_readonly()`.
544 :param key: Field key/name as string.
545 """
546 if self.readonly_fields:
547 if key in self.readonly_fields:
548 return True
549 return False
551 def set_required(self, key, required=True):
552 """
553 Enable or disable the "required" flag for a given field.
555 When a field is marked required, a value must be provided
556 or else it fails validation.
558 In practice if a field is "not required" then a default
559 "empty" value is assumed, should the user not provide one.
561 See also :meth:`is_required()`; this is tracked via
562 :attr:`required_fields`.
564 :param key: String key (fieldname) for the field.
566 :param required: New required flag for the field. Usually a
567 boolean, but may also be ``None`` to remove any flag and
568 revert to default behavior for the field.
569 """
570 self.required_fields[key] = required
572 def is_required(self, key):
573 """
574 Returns boolean indicating if the given field is marked as
575 required.
577 See also :meth:`set_required()`.
579 :param key: Field key/name as string.
581 :returns: Value for the flag from :attr:`required_fields` if
582 present; otherwise ``None``.
583 """
584 return self.required_fields.get(key, None)
586 def set_label(self, key, label):
587 """
588 Set the label for given field name.
590 See also :meth:`get_label()`.
591 """
592 self.labels[key] = label
594 # update schema if necessary
595 if self.schema and key in self.schema:
596 self.schema[key].title = label
598 def get_label(self, key):
599 """
600 Get the label for given field name.
602 Note that this will always return a string, auto-generating
603 the label if needed.
605 See also :meth:`set_label()`.
606 """
607 return self.labels.get(key, self.app.make_title(key))
609 def get_fields(self):
610 """
611 Returns the official list of field names for the form, or
612 ``None``.
614 If :attr:`fields` is set and non-empty, it is returned.
616 Or, if :attr:`schema` is set, the field list is derived
617 from that.
619 Or, if :attr:`model_class` is set, the field list is derived
620 from that, via :meth:`get_model_fields()`.
622 Otherwise ``None`` is returned.
623 """
624 if hasattr(self, 'fields') and self.fields:
625 return self.fields
627 if self.schema:
628 return [field.name for field in self.schema]
630 fields = self.get_model_fields()
631 if fields:
632 return fields
634 return []
636 def get_model_fields(self, model_class=None):
637 """
638 This method is a shortcut which calls
639 :func:`~wuttaweb.util.get_model_fields()`.
641 :param model_class: Optional model class for which to return
642 fields. If not set, the form's :attr:`model_class` is
643 assumed.
644 """
645 return get_model_fields(self.config,
646 model_class=model_class or self.model_class)
648 def get_schema(self):
649 """
650 Return the :class:`colander:colander.Schema` object for the
651 form, generating it automatically if necessary.
653 Note that if :attr:`schema` is already set, that will be
654 returned as-is.
655 """
656 if not self.schema:
658 ##############################
659 # create schema
660 ##############################
662 # get fields
663 fields = self.get_fields()
664 if not fields:
665 raise NotImplementedError
667 if self.model_class:
669 # collect list of field names and/or nodes
670 includes = []
671 for key in fields:
672 if key in self.nodes:
673 includes.append(self.nodes[key])
674 else:
675 includes.append(key)
677 # make initial schema with ColanderAlchemy magic
678 schema = SQLAlchemySchemaNode(self.model_class,
679 includes=includes)
681 # fill in the blanks if anything got missed
682 for key in fields:
683 if key not in schema:
684 node = colander.SchemaNode(colander.String(), name=key)
685 schema.add(node)
687 else:
689 # make basic schema
690 schema = colander.Schema()
691 for key in fields:
692 node = None
694 # use node override if present
695 if key in self.nodes:
696 node = self.nodes[key]
697 if not node:
699 # otherwise make simple string node
700 node = colander.SchemaNode(
701 colander.String(),
702 name=key)
704 schema.add(node)
706 ##############################
707 # customize schema
708 ##############################
710 # apply widget overrides
711 for key, widget in self.widgets.items():
712 if key in schema:
713 schema[key].widget = widget
715 # apply validator overrides
716 for key, validator in self.validators.items():
717 if key is None:
718 # nb. this one is form-wide
719 schema.validator = validator
720 elif key in schema: # field-level
721 schema[key].validator = validator
723 # apply default value overrides
724 for key, value in self.defaults.items():
725 if key in schema:
726 schema[key].default = value
728 # apply required flags
729 for key, required in self.required_fields.items():
730 if key in schema:
731 if required is False:
732 schema[key].missing = colander.null
734 self.schema = schema
736 return self.schema
738 def get_deform(self):
739 """
740 Return the :class:`deform:deform.Form` instance for the form,
741 generating it automatically if necessary.
742 """
743 if not hasattr(self, 'deform_form'):
744 model = self.app.model
745 schema = self.get_schema()
746 kwargs = {}
748 if self.model_instance:
750 # TODO: i keep finding problems with this, not sure
751 # what needs to happen. some forms will have a simple
752 # dict for model_instance, others will have a proper
753 # SQLAlchemy object. and in the latter case, it may
754 # not be "wutta-native" but from another DB.
756 # so the problem is, how to detect whether we should
757 # use the model_instance as-is or if we should convert
758 # to a dict. some options include:
760 # - check if instance has dictify() method
761 # i *think* this was tried and didn't work? but do not recall
763 # - check if is instance of model.Base
764 # this is unreliable since model.Base is wutta-native
766 # - check if form has a model_class
767 # has not been tried yet
769 # - check if schema is from colanderalchemy
770 # this is what we are trying currently...
772 if isinstance(schema, SQLAlchemySchemaNode):
773 kwargs['appstruct'] = schema.dictify(self.model_instance)
774 else:
775 kwargs['appstruct'] = self.model_instance
777 # create the Deform instance
778 # nb. must give a reference back to wutta form; this is
779 # for sake of field schema nodes and widgets, e.g. to
780 # access the main model instance
781 form = deform.Form(schema, **kwargs)
782 form.wutta_form = self
783 self.deform_form = form
785 return self.deform_form
787 def render_vue_tag(self, **kwargs):
788 """
789 Render the Vue component tag for the form.
791 By default this simply returns:
793 .. code-block:: html
795 <wutta-form></wutta-form>
797 The actual output will depend on various form attributes, in
798 particular :attr:`vue_tagname`.
799 """
800 return HTML.tag(self.vue_tagname, **kwargs)
802 def render_vue_template(
803 self,
804 template='/forms/vue_template.mako',
805 **context):
806 """
807 Render the Vue template block for the form.
809 This returns something like:
811 .. code-block:: none
813 <script type="text/x-template" id="wutta-form-template">
814 <form>
815 <!-- fields etc. -->
816 </form>
817 </script>
819 <script>
820 WuttaFormData = {}
821 WuttaForm = {
822 template: 'wutta-form-template',
823 }
824 </script>
826 .. todo::
828 Why can't Sphinx render the above code block as 'html' ?
830 It acts like it can't handle a ``<script>`` tag at all?
832 Actual output will of course depend on form attributes, i.e.
833 :attr:`vue_tagname` and :attr:`fields` list etc.
835 :param template: Path to Mako template which is used to render
836 the output.
837 """
838 context['form'] = self
839 context['dform'] = self.get_deform()
840 context.setdefault('form_attrs', {})
841 context.setdefault('request', self.request)
842 context['model_data'] = self.get_vue_model_data()
844 # auto disable button on submit
845 if self.auto_disable_submit:
846 context['form_attrs']['@submit'] = 'formSubmitting = true'
848 output = render(template, context)
849 return HTML.literal(output)
851 def add_grid_vue_context(self, grid):
852 """ """
853 if not grid.key:
854 raise ValueError("grid must have a key!")
856 if grid.key in self.grid_vue_context:
857 log.warning("grid data with key '%s' already registered, "
858 "but will be replaced", grid.key)
860 self.grid_vue_context[grid.key] = grid.get_vue_context()
862 def render_vue_field(
863 self,
864 fieldname,
865 readonly=None,
866 **kwargs,
867 ):
868 """
869 Render the given field completely, i.e. ``<b-field>`` wrapper
870 with label and containing a widget.
872 Actual output will depend on the field attributes etc.
873 Typical output might look like:
875 .. code-block:: html
877 <b-field label="Foo"
878 horizontal
879 type="is-danger"
880 message="something went wrong!">
881 <!-- widget element(s) -->
882 </b-field>
884 .. warning::
886 Any ``**kwargs`` received from caller are ignored by this
887 method. For now they are allowed, for sake of backwawrd
888 compatibility. This may change in the future.
889 """
890 # readonly comes from: caller, field flag, or form flag
891 if readonly is None:
892 readonly = self.is_readonly(fieldname)
893 if not readonly:
894 readonly = self.readonly
896 # but also, fields not in deform/schema must be readonly
897 dform = self.get_deform()
898 if not readonly and fieldname not in dform:
899 readonly = True
901 # render the field widget or whatever
902 if fieldname in dform:
904 # render proper widget if field is in deform/schema
905 field = dform[fieldname]
906 kw = {}
907 if readonly:
908 kw['readonly'] = True
909 html = field.serialize(**kw)
911 else:
912 # render static text if field not in deform/schema
913 # TODO: need to abstract this somehow
914 if self.model_instance:
915 html = str(self.model_instance[fieldname])
916 else:
917 html = ''
919 # mark all that as safe
920 html = HTML.literal(html)
922 # render field label
923 label = self.get_label(fieldname)
925 # b-field attrs
926 attrs = {
927 ':horizontal': 'true',
928 'label': label,
929 }
931 # next we will build array of messages to display..some
932 # fields always show a "helptext" msg, and some may have
933 # validation errors..
934 field_type = None
935 messages = []
937 # show errors if present
938 errors = self.get_field_errors(fieldname)
939 if errors:
940 field_type = 'is-danger'
941 messages.extend(errors)
943 # ..okay now we can declare the field messages and type
944 if field_type:
945 attrs['type'] = field_type
946 if messages:
947 cls = 'is-size-7'
948 if field_type == 'is-danger':
949 cls += ' has-text-danger'
950 messages = [HTML.tag('p', c=[msg], class_=cls)
951 for msg in messages]
952 slot = HTML.tag('slot', name='messages', c=messages)
953 html = HTML.tag('div', c=[html, slot])
955 return HTML.tag('b-field', c=[html], **attrs)
957 def render_vue_finalize(self):
958 """
959 Render the Vue "finalize" script for the form.
961 By default this simply returns:
963 .. code-block:: html
965 <script>
966 WuttaForm.data = function() { return WuttaFormData }
967 Vue.component('wutta-form', WuttaForm)
968 </script>
970 The actual output may depend on various form attributes, in
971 particular :attr:`vue_tagname`.
972 """
973 set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
974 make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
975 return HTML.tag('script', c=['\n',
976 HTML.literal(set_data),
977 '\n',
978 HTML.literal(make_component),
979 '\n'])
981 def get_vue_model_data(self):
982 """
983 Returns a dict with form model data. Values may be nested
984 depending on the types of fields contained in the form.
986 This collects the ``cstruct`` values for all fields which are
987 present both in :attr:`fields` as well as the Deform schema.
989 It also converts each as needed, to ensure it is
990 JSON-serializable.
992 :returns: Dict of field/value items.
993 """
994 dform = self.get_deform()
995 model_data = {}
997 def assign(field):
998 value = field.cstruct
1000 # TODO: we need a proper true/false on the Vue side,
1001 # but deform/colander want 'true' and 'false' ..so
1002 # for now we explicitly translate here, ugh. also
1003 # note this does not yet allow for null values.. :(
1004 if isinstance(field.typ, colander.Boolean):
1005 value = True if field.typ.true_val else False
1007 model_data[field.oid] = make_json_safe(value)
1009 for key in self.fields:
1011 # TODO: i thought commented code was useful, but no longer sure?
1013 # TODO: need to describe the scenario when this is true
1014 if key not in dform:
1015 # log.warning("field '%s' is missing from deform", key)
1016 continue
1018 field = dform[key]
1020 # if hasattr(field, 'children'):
1021 # for subfield in field.children:
1022 # assign(subfield)
1024 assign(field)
1026 return model_data
1028 # TODO: for tailbone compat, should document?
1029 # (ideally should remove this and find a better way)
1030 def get_vue_field_value(self, key):
1031 """ """
1032 if key not in self.fields:
1033 return
1035 dform = self.get_deform()
1036 if key not in dform:
1037 return
1039 field = dform[key]
1040 return make_json_safe(field.cstruct)
1042 def validate(self):
1043 """
1044 Try to validate the form, using data from the :attr:`request`.
1046 Uses :func:`~wuttaweb.util.get_form_data()` to retrieve the
1047 form data from POST or JSON body.
1049 If the form data is valid, the data dict is returned. This
1050 data dict is also made available on the form object via the
1051 :attr:`validated` attribute.
1053 However if the data is not valid, ``False`` is returned, and
1054 there will be no :attr:`validated` attribute. In that case
1055 you should inspect the form errors to learn/display what went
1056 wrong for the user's sake. See also
1057 :meth:`get_field_errors()`.
1059 This uses :meth:`deform:deform.Field.validate()` under the
1060 hood.
1062 .. warning::
1064 Calling ``validate()`` on some forms will cause the
1065 underlying Deform and Colander structures to mutate. In
1066 particular, all :attr:`readonly_fields` will be *removed*
1067 from the :attr:`schema` to ensure they are not involved in
1068 the validation.
1070 :returns: Data dict, or ``False``.
1071 """
1072 if hasattr(self, 'validated'):
1073 del self.validated
1075 if self.request.method != 'POST':
1076 return False
1078 # remove all readonly fields from deform / schema
1079 dform = self.get_deform()
1080 if self.readonly_fields:
1081 schema = self.get_schema()
1082 for field in self.readonly_fields:
1083 if field in schema:
1084 del schema[field]
1085 dform.children.remove(dform[field])
1087 # let deform do real validation
1088 controls = get_form_data(self.request).items()
1089 try:
1090 self.validated = dform.validate(controls)
1091 except deform.ValidationFailure:
1092 log.debug("form not valid: %s", dform.error)
1093 return False
1095 return self.validated
1097 def get_field_errors(self, field):
1098 """
1099 Return a list of error messages for the given field.
1101 Not useful unless a call to :meth:`validate()` failed.
1102 """
1103 dform = self.get_deform()
1104 if field in dform:
1105 field = dform[field]
1106 if field.error:
1107 return field.error.messages()
1108 return []