Coverage for portality/lib/formulaic.py: 67%
615 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-22 15:59 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-22 15:59 +0100
1# ~~Formulaic:Library~~
3"""
4EXAMPLE = {
5 "contexts" : {
6 "[context name]" : {
7 "fieldsets" : [
8 "[fieldset name]"
9 ],
10 "asynchronous_warnings" : [
11 "[warning function reference]"
12 ]
13 }
14 },
15 "fieldsets" : {
16 "[fieldset name]" : {
17 "label" : "[label]",
18 "fields" : [
19 "[field name]"
20 ]
21 }
22 },
23 "fields" : {
24 "[field name]" : {
25 "label" : "[label]",
26 "input" : "[input type: radio|text|taglist|select|checkbox]",
27 "multiple" : "[select multiple True|False]",
28 "datatype" : "[coerce datatype]",
29 "options" : [ # for radio, select and checkbox
30 {
31 "display" : "[display value]",
32 "value" : "[stored value]",
33 "exclusive" : "[exclusive: True|False]", # if you select this option you can't select others
34 "subfields" : ["[field name]"]
35 }
36 ],
37 "options_fn" : "function name to generate options",
38 "default" : "[default value]",
39 "disabled" : "[disabled: True|False OR a function reference string]",
40 "conditional" : [ # conditions to AND together
41 {
42 "field" : "[field name]",
43 "value" : "[field value]"
44 }
45 ],
46 "help" : {
47 "placeholder" : "[input field placeholder text]",
48 "description" : "[description]",
49 "tooltip" : "[tooltip/long description]",
50 "doaj_criteria" : "[doaj compliance criteria]",
51 "seal_criteria" : "[doaj compliance criteria]"
52 },
53 "validate" : {
54 "[validate function]",
55 {"[validate function]" : {"arg" : "argval"}}
56 },
57 "widgets" : {
58 "[widget function]",
59 {"[widget function]" : {"arg" : "argval"}}
60 },
61 "postprocessing" : {
62 "[processing function]",
63 {"[processing function]" : {"arg" : "argval"}}
64 },
65 "attr" : {
66 "[html attribute name]" : "[html attribute value]"
67 },
68 "contexts" : {
69 "[context name]" : {
70 "[field property]" : "[field property value]"
71 }
72 }
73 }
74 }
75}
77CONTEXT_EXAMPLE = {
78 "fieldsets" : [
79 {
80 "name" : "[fieldset name]",
81 "label" : "[label]",
82 "fields" : [
83 {
84 "name" : "[field name]",
85 "[field property]" : "[field property value]"
86 }
87 ]
88 }
89 ],
90 "asynchronous_warnings" : [
91 "[warning function reference]"
92 ],
93 "template" : "path/to/form/page/template.html",
94 "crosswalks" : {
95 "obj2form" : "crosswalk.obj2form",
96 "form2obj" : "crosswalk.form2obj"
97 },
98 "processor" : "module.path.to.processor"
99}
100"""
101import csv
102from copy import deepcopy
103from wtforms import Form
104from wtforms.fields.core import UnboundField, FieldList, FormField
106from portality.lib import plugin
107from flask import render_template
108import json
111UI_CONFIG_FIELDS = [
112 "label",
113 "input",
114 "options",
115 "help",
116 "validate",
117 "visible",
118 "conditional",
119 "widgets",
120 "attr",
121 "multiple",
122 "repeatable",
123 "datatype",
124 "disabled",
125 "name",
126 "subfields",
127 "subfield",
128 "group"
129]
132class FormulaicException(Exception):
133 def __init__(self, *args):
134 try:
135 self.message = args[0]
136 except IndexError:
137 self.message = ''
138 super(FormulaicException, self).__init__(*args)
141class Formulaic(object):
142 def __init__(self, definition, wtforms_builders, function_map=None, javascript_functions=None):
143 self._definition = definition
144 self._wtforms_builders = wtforms_builders
145 self._function_map = function_map
146 self._javascript_functions = javascript_functions
148 def context(self, context_name):
149 context_def = deepcopy(self._definition.get("contexts", {}).get(context_name))
150 if context_def is None:
151 return None
153 fieldsets = context_def.get("fieldsets", [])
155 expanded_fieldsets = []
156 for fsn in fieldsets:
157 fieldset_def = deepcopy(self._definition.get("fieldsets", {}).get(fsn))
158 if fieldset_def is None:
159 raise FormulaicException("Unable to locate fieldset with name {x} in context {y}".format(x=fsn, y=context_name))
160 fieldset_def["name"] = fsn
162 expanded_fields = self._process_fields(context_name, fieldset_def.get("fields", []))
164 fieldset_def["fields"] = expanded_fields
165 expanded_fieldsets.append(fieldset_def)
167 context_def["fieldsets"] = expanded_fieldsets
168 return FormulaicContext(context_name, context_def, self)
170 @property
171 def wtforms_builders(self):
172 return self._wtforms_builders
174 @property
175 def function_map(self):
176 return self._function_map
178 @property
179 def javascript_functions(self):
180 return self._javascript_functions
182 def choices_for(self, field_name, formulaic_context=None):
183 field_def = self._definition.get("fields", {}).get(field_name)
184 if field_def is None:
185 return []
187 return FormulaicField._options2choices(field_def, formulaic_context)
189 def _process_fields(self, context_name, field_names):
190 field_defs = []
191 for fn in field_names:
192 field_def = deepcopy(self._definition.get("fields", {}).get(fn))
193 field_def["name"] = fn
194 if field_def is None:
195 raise FormulaicException("Field '{x}' is referenced but not defined".format(x=fn))
197 # filter for context
198 context_overrides = field_def.get("contexts", {}).get(context_name)
199 if context_overrides is not None:
200 for k, v in context_overrides.items():
201 field_def[k] = v
203 # and remove the context overrides settings, so they don't bleed to contexts that don't require them
204 if "contexts" in field_def:
205 del field_def["contexts"]
207 field_defs.append(field_def)
209 return field_defs
211 @classmethod
212 def run_options_fn(cls, field_def, formulaic_context):
213 # opt_fn = options_function_map.get(field_def["options_fn"])
214 opt_fn = formulaic_context.function_map.get("options", {}).get(field_def.get("options_fn"))
215 if opt_fn is None:
216 raise FormulaicException(
217 "No function mapping defined for function reference '{x}'".format(x=field_def["options_fn"]))
218 if isinstance(opt_fn, str):
219 opt_fn = plugin.load_function(opt_fn)
220 return opt_fn(field_def, formulaic_context)
223# ~~->$ FormulaicContext:Feature~~
224class FormulaicContext(object):
225 def __init__(self, name, definition, parent: Formulaic):
226 self._name = name
227 self._definition = definition
228 self._formulaic = parent
229 self._wtform_class = None
230 self._wtform_inst = None
232 self._wtform_class = self.wtform_class()
233 self._wtform_inst = self.wtform()
235 @property
236 def name(self):
237 return self._name
239 @property
240 def wtforms_builders(self):
241 return self._formulaic.wtforms_builders
243 @property
244 def function_map(self):
245 return self._formulaic.function_map
247 @property
248 def wtform_inst(self):
249 return self._wtform_inst
251 @property
252 def javascript_functions(self):
253 return self._formulaic.javascript_functions
255 @property
256 def ui_settings(self):
257 ui = deepcopy(self._definition.get("fieldsets", []))
258 for fieldset in ui:
259 for field in fieldset.get("fields", []):
260 for fn in [k for k in field.keys()]:
261 if fn not in UI_CONFIG_FIELDS:
262 del field[fn]
263 return ui
265 @property
266 def default_field_template(self):
267 return self._definition.get("templates", {}).get("default_field")
269 @property
270 def default_group_template(self):
271 return self._definition.get("templates", {}).get("default_group")
273 def list_fields_in_order(self):
274 fieldlist = []
275 for fs in self.fieldsets():
276 for field in fs.fields():
277 fieldlist.append(field)
278 if field.group_subfields():
279 for sf in field.group_subfields():
280 fieldlist.append(sf)
281 return fieldlist
283 def make_wtform_class(self, fields):
284 # ~~^-> WTForms:Library~~
285 class TempForm(Form):
286 pass
288 for field in fields:
289 self.bind_wtforms_field(TempForm, field)
291 return TempForm
293 def wtform_class(self):
294 if self._definition is None:
295 return
297 if self._wtform_class is not None:
298 return self._wtform_class
300 # FIXME: we should just store a list of fields in the context, and reference them
301 # from the fieldset, which would get rid of a lot of this double-layered looping
302 fields = []
303 for fieldset in self._definition.get("fieldsets", []):
304 for field in fieldset.get("fields", []):
305 if "group" in field:
306 continue
307 #if "subfield" in field:
308 # continue
309 fields.append(field)
311 klazz = self.make_wtform_class(fields)
312 self._wtform_class = klazz
313 return self._wtform_class
315 def wtform(self, formdata=None, data=None):
316 if self._definition is None:
317 return
319 klazz = self.wtform_class()
320 self._wtform_inst = klazz(formdata=formdata, data=data)
322 if formdata is not None or data is not None:
323 for fs in self._definition.get("fieldsets", []):
324 for f in fs.get("fields", []):
325 if "options_fn" in f:
326 opts = FormulaicField._options2choices(f, self)
327 wtf = None
328 if f.get("group") is not None:
329 wtf = self._wtform_inst[f["group"]]
330 if isinstance(wtf, FieldList):
331 for entry in wtf:
332 entry[f["name"]].choices = opts
333 else:
334 wtf[f["name"]].choices = opts
335 else:
336 wtf = self._wtform_inst[f["name"]]
337 if isinstance(wtf, FieldList):
338 for entry in wtf:
339 entry.choices = opts
340 else:
341 wtf.choices = opts
343 return self._wtform_inst
345 def get(self, field_name, parent=None):
346 if parent is None:
347 parent = self
348 for fs in self._definition.get("fieldsets", []):
349 for f in fs.get("fields", []):
350 if f.get("name") == field_name:
351 return FormulaicField(f, parent)
353 def repeatable_fields(self, parent=None):
354 if parent is None:
355 parent = self
357 reps = []
358 for fs in self._definition.get("fieldsets", []):
359 for f in fs.get("fields", []):
360 if "repeatable" in f:
361 reps.append(FormulaicField(f, parent))
363 return reps
365 def disabled_fields(self, parent=None):
366 if parent is None:
367 parent = self
369 disableds = []
370 for fs in self._definition.get("fieldsets", []):
371 for f in fs.get("fields", []):
372 field = FormulaicField(f, parent)
373 if field.is_disabled:
374 disableds.append(field)
376 return disableds
378 def fieldset(self, fieldset_name):
379 for fs in self._definition.get("fieldsets", []):
380 if fs.get("name") == fieldset_name:
381 return FormulaicFieldset(fs, self)
382 return None
384 def fieldsets(self):
385 return [FormulaicFieldset(fs, self) for fs in self._definition.get("fieldsets", [])]
387 def json(self):
388 return json.dumps(self._definition)
390 def render_template(self, **kwargs):
391 # ~~^-> Jinja2:Technology~~
392 template = self._definition.get("templates", {}).get("form")
393 return render_template(template, formulaic_context=self, **kwargs)
395 def processor(self, formdata=None, source=None):
396 # ~~^-> FormProcessor:Feature~~
397 klazz = self._definition.get("processor")
398 if isinstance(klazz, str):
399 klazz = plugin.load_class(klazz)
400 return klazz(formdata=formdata, source=source, parent=self)
402 def obj2form(self, obj):
403 # ~~^-> Crosswalk:Feature~~
404 xwalk_fn = self._definition.get("crosswalks", {}).get("obj2form")
405 if xwalk_fn is None:
406 return None
407 if isinstance(xwalk_fn, str):
408 xwalk_fn = plugin.load_function(xwalk_fn)
409 data = xwalk_fn(obj)
410 return self.wtform(data=data)
412 def form2obj(self):
413 # ~~^-> Crosswalk:Feature~~
414 xwalk_fn = self._definition.get("crosswalks", {}).get("form2obj")
415 if xwalk_fn is None:
416 return None
417 if isinstance(xwalk_fn, str):
418 xwalk_fn = plugin.load_function(xwalk_fn)
419 return xwalk_fn(self._wtform_inst)
421 def bind_wtforms_field(self, FormClass, field):
422 field_name = field.get("name")
423 if not hasattr(FormClass, field_name):
424 field_definition = FormulaicField.make_wtforms_field(self, field)
425 setattr(FormClass, field_name, field_definition)
427 def to_summary_csv(self, out_file):
428 def _make_row(i, fs, parent, field, writer):
429 options = ""
430 if field.explicit_options:
431 options = "; ".join([o.get("display") + " (" + o.get("value") + ")" for o in field.explicit_options])
432 elif field.options_fn_name:
433 options = field.options_fn_name
435 label = ""
436 if hasattr(field, "label"):
437 label = field.label
439 name = ""
440 if parent is not None and hasattr(parent, "name"):
441 name = parent.name + "."
442 if hasattr(field, "name"):
443 name += field.name
445 writer.writerow([
446 i,
447 name,
448 label,
449 field.input,
450 options,
451 field.is_disabled,
452 fs.name
453 ])
455 with open(out_file, "w", encoding="utf-8") as f:
456 writer = csv.writer(f)
457 writer.writerow(["Form Position", "Field Name", "Label", "Input Type", "Options", "Disabled?", "Fieldset ID"])
458 i = 1
459 for fs in self.fieldsets():
460 for field in fs.fields():
461 _make_row(i, fs, None, field, writer)
462 i += 1
463 if field.group_subfields():
464 for sf in field.group_subfields():
465 _make_row(i, fs, field, sf, writer)
466 i += 1
468# ~~->$ FormulaicFieldset:Feature~~
469class FormulaicFieldset(object):
470 def __init__(self, definition, parent):
471 self._definition = definition
472 self._formulaic_context = parent
474 @property
475 def wtforms_builders(self):
476 return self._formulaic_context.wtforms_builders
478 @property
479 def function_map(self):
480 return self._formulaic_context.function_map
482 @property
483 def wtform_inst(self):
484 return self._formulaic_context.wtform_inst
486 @property
487 def default_field_template(self):
488 return self._formulaic_context.default_field_template
490 @property
491 def default_group_template(self):
492 return self._formulaic_context.default_group_template
494 def fields(self):
495 return [FormulaicField(f, self) for f in
496 self._definition.get("fields", []) if not f.get("group")]
498 def field(self, field_name):
499 for f in self._definition.get("fields", []):
500 if f.get("name") == field_name:
501 return FormulaicField(f, self)
503 def __getattr__(self, name):
504 if hasattr(self.__class__, name):
505 return object.__getattribute__(self, name)
507 if name in self._definition:
508 return self._definition[name]
510 raise AttributeError('{name} is not set'.format(name=name))
512# ~~->$ FormulaicField:Feature~~
513class FormulaicField(object):
514 def __init__(self, definition, parent):
515 self._definition = definition
516 self._formulaic_fieldset = parent
518 def __contains__(self, item):
519 return item in self._definition
521 def __getattr__(self, name):
522 if hasattr(self.__class__, name):
523 return object.__getattribute__(self, name)
525 if name in self._definition:
526 return self._definition[name]
528 raise AttributeError('{name} is not set'.format(name=name))
530 @property
531 def parent_context(self):
532 if isinstance(self._formulaic_fieldset, FormulaicContext):
533 return self._formulaic_fieldset
534 return self._formulaic_fieldset._formulaic_context
536 def get(self, attr, default=None):
537 return self._definition.get(attr, default)
539 def help(self, key, default=None):
540 return self._definition.get("help", {}).get(key, default)
542 @property
543 def optional(self):
544 return self._definition.get("optional", False)
546 @property
547 def wtforms_builders(self):
548 return self._formulaic_fieldset.wtforms_builders
550 @property
551 def function_map(self):
552 return self._formulaic_fieldset.function_map
554 @property
555 def wtform_inst(self):
556 if "group" in self._definition:
557 group = self._definition["group"]
558 group_field = self._formulaic_fieldset.field(group)
559 return group_field.wtfield
560 return self._formulaic_fieldset.wtform_inst
562 @property
563 def wtfield(self):
564 name = self._definition.get("name")
565 if self.wtform_inst is None:
566 return None
567 if hasattr(self.wtform_inst, name):
568 return getattr(self.wtform_inst, name)
569 return None
571 @property
572 def explicit_options(self):
573 opts = self._definition.get("options", [])
574 if isinstance(opts, list):
575 return opts
576 return []
578 @property
579 def options_fn_name(self):
580 return self._definition.get("options_fn")
582 @property
583 def is_disabled(self):
584 differently_abled = self._definition.get("disabled", False)
585 if isinstance(differently_abled, str):
586 fn = self.function_map.get("disabled", {}).get(differently_abled)
587 differently_abled = fn(self, self.parent_context)
588 return differently_abled
590 @property
591 def has_conditional(self):
592 return len(self._definition.get("conditional", [])) > 0
594 @property
595 def condition_field(self):
596 condition = self._definition.get("conditional", [])
597 return condition[0].get("field", None) if len(condition) > 0 else None
599 @property
600 def condition_value(self):
601 condition = self._definition.get("conditional", [])
602 return condition[0].get("value", None) if len(condition) > 0 else None
604 @property
605 def template(self):
606 local = self._definition.get("template")
607 if local is not None:
608 return local
610 if self._definition.get("input") == "group":
611 return self._formulaic_fieldset.default_group_template
613 return self._formulaic_fieldset.default_field_template
615 @property
616 def entry_template(self):
617 return self._definition.get("entry_template")
619 def has_validator(self, validator_name):
620 for validator in self._definition.get("validate", []):
621 if isinstance(validator, str) and validator == validator_name:
622 return True
623 if isinstance(validator, dict):
624 if list(validator.keys())[0] == validator_name:
625 return True
626 return False
628 def get_validator_settings(self, validator_name):
629 for validator in self._definition.get("validate", []):
630 if isinstance(validator, str) and validator == validator_name:
631 return {}
632 if isinstance(validator, dict):
633 name = list(validator.keys())[0]
634 if name == validator_name:
635 return validator[name]
636 return False
638 def validators(self):
639 for validator in self._definition.get("validate", []):
640 if isinstance(validator, str):
641 yield validator, {}
642 if isinstance(validator, dict):
643 name = list(validator.keys())[0]
644 yield name, validator[name]
646 def get_subfields(self, option_value):
647 for option in self.explicit_options:
648 if option.get("value") == option_value:
649 sfs = []
650 for sf in option.get("subfields", []):
651 subimpl = self._formulaic_fieldset._formulaic_context.get(sf, self._formulaic_fieldset)
652 sfs.append(subimpl)
653 return sfs
655 def group_subfields(self):
656 subs = self._definition.get("subfields")
657 if subs is None:
658 return None
659 return [self._formulaic_fieldset.field(s) for s in subs]
661 def has_options_subfields(self):
662 for option in self.explicit_options:
663 if len(option.get("subfields", [])) > 0:
664 return True
665 return False
667 def has_errors(self):
668 return len(self.errors()) > 0
670 def errors(self):
671 wtf = self.wtfield
672 return wtf.errors
674 def has_widget(self, widget_name):
675 for w in self._definition.get("widgets", []):
676 if w == widget_name:
677 return True
678 if isinstance(w, dict):
679 if widget_name in w:
680 return True
681 return False
683 def render_form_control(self, custom_args=None, wtfinst=None):
684 # ~~-> WTForms:Library~~
685 kwargs = deepcopy(self._definition.get("attr", {}))
686 if "placeholder" in self._definition.get("help", {}):
687 kwargs["placeholder"] = self._definition["help"]["placeholder"]
689 render_functions = self.function_map.get("validate", {}).get("render", {})
690 for validator, settings in self.validators():
691 if validator in render_functions:
692 fn = render_functions[validator]
693 if isinstance(fn, str):
694 fn = plugin.load_function(fn)
695 fn(settings, kwargs)
697 if self.has_options_subfields():
698 kwargs["formulaic"] = self
700 add_data_as_choice = False
701 if self.is_disabled:
702 kwargs["disabled"] = "disabled"
703 add_data_as_choice = True
705 # allow custom args to overwite all other arguments
706 if custom_args is not None:
707 for k, v in custom_args.items():
708 kwargs[k] = v
710 wtf = None
711 if wtfinst is not None:
712 wtf = wtfinst
713 else:
714 wtf = self.wtfield
716 if add_data_as_choice and hasattr(wtf, "choices") and wtf.data not in [c[0] for c in wtf.choices]:
717 wtf.choices += [(wtf.data, wtf.data)]
719 return wtf(**kwargs)
721 @classmethod
722 def make_wtforms_field(cls, formulaic_context, field) -> UnboundField:
723 builder = cls._get_wtforms_builder(field, formulaic_context.wtforms_builders)
724 if builder is None:
725 raise FormulaicException("No WTForms mapping for field '{x}'".format(x=field.get("name")))
727 validators = []
728 vfuncs = formulaic_context.function_map.get("validate", {}).get("wtforms", {})
729 for v in field.get("validate", []):
730 vname = v
731 args = {}
732 if isinstance(v, dict):
733 vname = list(v.keys())[0]
734 args = v[vname]
735 if vname not in vfuncs:
736 raise FormulaicException("No validate WTForms function defined for {x} in python function references".format(x=vname))
737 vfn = vfuncs[vname]
738 if isinstance(vfn, str):
739 vfn = plugin.load_function(vfn)
740 validators.append(vfn(field, args))
742 wtargs = {
743 "label" : field.get("label"),
744 "validators" : validators,
745 "description": field.get("help", {}).get("description"),
746 }
747 if "default" in field:
748 wtargs["default"] = field.get("default")
749 if "options" in field or "options_fn" in field:
750 wtargs["choices"] = cls._options2choices(field, formulaic_context)
752 return builder(formulaic_context, field, wtargs)
754 @classmethod
755 def _get_wtforms_builder(self, field, wtforms_builders):
756 for builder in wtforms_builders:
757 if builder.match(field):
758 return builder.wtform
759 return None
761 @classmethod
762 def _options2choices(self, field, formulaic_context):
763 # function_map = formulaic_context.function_map.get("options", {})
765 options = field.get("options", [])
766 if len(options) == 0 and "options_fn" in field:
767 # options = Formulaic.run_options_fn(field, formulaic_context)
768 options = Formulaic.run_options_fn(field, formulaic_context)
769 # fnpath = function_map.get(field["options_fn"])
770 # if fnpath is None:
771 # raise FormulaicException("No function mapping defined for function reference '{x}'".format(x=field["options_fn"]))
772 # fn = plugin.load_function(fnpath)
773 # options = fn(field, formulaic_context.name)
775 choices = []
776 for o in options:
777 display = o.get("display")
778 value = o.get("value")
779 if value is None:
780 value = o.get("display")
781 choices.append((value, display))
783 return choices
785# ~~->$ FormProcessor:Feature~~
786class FormProcessor(object):
787 def __init__(self, formdata=None, source=None, parent: FormulaicContext=None):
788 # initialise our core properties
789 self._source = source
790 self._target = None
791 self._formdata = formdata
792 self._alert = []
793 self._info = ''
794 self._formulaic = parent
796 # now create our form instance, with the form_data (if there is any)
797 if formdata is not None:
798 self.data2form()
800 # if there isn't any form data, then we should create the form properties from source instead
801 elif source is not None:
802 self.source2form()
804 # if there is no source, then a blank form object
805 else:
806 self.blank_form()
808 ############################################################
809 # getters and setters on the main FormContext properties
810 ############################################################
812 @property
813 def form(self):
814 # return self._form
815 return self._formulaic.wtform_inst
817 @property
818 def source(self):
819 return self._source
821 @property
822 def form_data(self):
823 return self._formdata
825 @property
826 def target(self):
827 return self._target
829 @target.setter
830 def target(self, val):
831 self._target = val
833 # @property
834 # def template(self):
835 # return self._template
836 #
837 # @template.setter
838 # def template(self, val):
839 # self._template = val
841 @property
842 def alert(self):
843 return self._alert
845 def add_alert(self, val):
846 self._alert.append(val)
848 @property
849 def info(self):
850 return self._info
852 @info.setter
853 def info(self, val):
854 self._info = val
856 #############################################################
857 # Lifecycle functions you don't have to overwrite unless you
858 # want to do something different
859 #############################################################
861 def blank_form(self):
862 """
863 This will be called during init, and must populate the self.form_data property with an instance of the form in this
864 context, based on no originating source or form data
865 """
866 self._formulaic.wtform()
868 def data2form(self):
869 """
870 This will be called during init, and must convert the form_data into an instance of the form in this context,
871 and write to self.form
872 """
873 self._formulaic.wtform(formdata=self.form_data)
875 def source2form(self):
876 """
877 This will be called during init, and must convert the source object into an instance of the form in this
878 context, and write to self.form
879 """
880 self._formulaic.obj2form(self.source)
882 def form2target(self):
883 """
884 Convert the form object into a the target system object, and write to self.target
885 """
886 self.target = self._formulaic.form2obj()
888 ############################################################
889 # Lifecycle functions that subclasses should implement
890 ############################################################
892 def pre_validate(self):
893 """
894 This will be run before validation against the form is run.
895 Use it to patch the form with any relevant data, such as fields which were disabled
896 """
897 repeatables = self._formulaic.repeatable_fields()
898 for repeatable in repeatables:
899 wtf = repeatable.wtfield
900 if not isinstance(wtf, FieldList):
901 continue
903 # get all of the entries off the field list, leaving the field list temporarily empty
904 entries = []
905 for i in range(len(wtf.entries)):
906 entries.append(wtf.pop_entry())
908 # go through each entry, and if it has any data in it put it back onto the list, otherwise
909 # leave it off
910 for entry in entries:
911 if isinstance(entry, FormField):
912 data = entry.data
913 has_data = False
914 for k, v in data.items():
915 if v:
916 has_data = True
917 break
918 if not has_data:
919 continue
920 else:
921 if not entry.data:
922 continue
923 wtf.append_entry(entry.data)
925 # finally, ensure that the minimum number of fields are populated in the list
926 min = repeatable.get("repeatable", {}).get("minimum", 0)
927 if min > len(wtf.entries):
928 for i in range(len(wtf.entries), min):
929 wtf.append_entry()
931 # patch over any disabled fields
932 disableds = self._formulaic.disabled_fields()
933 if len(disableds) > 0:
934 alt_formulaic = self._formulaic.__class__(self._formulaic.name, self._formulaic._definition, self._formulaic._formulaic)
935 other_processor = alt_formulaic.processor(source=self.source)
936 other_form = other_processor.form
937 for dis in disableds:
938 if dis.get("group") is not None:
939 dis = self._formulaic.get(dis.get("group"))
941 if dis.get("merge_disabled"):
942 self._merge_disabled(dis, other_form)
943 else:
944 wtf = dis.wtfield
945 wtf.data = other_form._fields[dis.get("name")].data
947 def _merge_disabled(self, disabled, other_form):
948 fnref = disabled.get("merge_disabled")
949 fn = self._formulaic.function_map.get("merge_disabled", {}).get(fnref)
950 if not fn:
951 raise FormulaicException("No merge_disabled function defined {x}".format(x=fnref))
952 fn(disabled, other_form)
954 def patch_target(self):
955 """
956 Patch the target with data from the source. This will be run by the finalise method (unless you override it)
957 """
958 pass
960 def finalise(self, *args, **kwargs):
961 """
962 Finish up with the FormContext. Carry out any final workflow tasks, etc.
963 """
964 self.form2target()
965 self.patch_target()
967 def draft(self, *args, **kwargs):
968 pass
970 ############################################################
971 # Functions which can be called directly, but may be overridden if desired
972 ############################################################
974 def validate(self):
975 self.pre_validate()
976 f = self.form
977 valid = False
978 if f is not None:
979 valid = f.validate()
980 return valid
982 @property
983 def errors(self):
984 f = self.form
985 if f is not None:
986 return f.errors
987 return False
990class WTFormsBuilder:
991 @staticmethod
992 def match(field):
993 return False
995 @staticmethod
996 def wtform(formulaic_context, field, wtfargs):
997 return None