Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/cardinal_pythonlib/deform_utils.py : 28%

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
1#!/usr/bin/env python
2# cardinal_pythonlib/deform_utils.py
4"""
5===============================================================================
7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com).
9 This file is part of cardinal_pythonlib.
11 Licensed under the Apache License, Version 2.0 (the "License");
12 you may not use this file except in compliance with the License.
13 You may obtain a copy of the License at
15 https://www.apache.org/licenses/LICENSE-2.0
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
23===============================================================================
24"""
26from typing import (Any, Callable, Dict, Generator, Iterable, List, Tuple,
27 TYPE_CHECKING)
29from cardinal_pythonlib.logs import get_brace_style_log_with_null_handler
30# noinspection PyUnresolvedReferences
31from colander import Invalid, SchemaNode
32from deform.exception import ValidationFailure
33from deform.field import Field
34from deform.form import Form
35from deform.widget import HiddenWidget
37if TYPE_CHECKING:
38 # noinspection PyUnresolvedReferences
39 from pyramid.request import Request
41log = get_brace_style_log_with_null_handler(__name__)
43ValidatorType = Callable[[SchemaNode, Any], None] # called as v(node, value)
45# =============================================================================
46# Debugging options
47# =============================================================================
49DEBUG_DYNAMIC_DESCRIPTIONS_FORM = False
50DEBUG_FORM_VALIDATION = False
52if any([DEBUG_DYNAMIC_DESCRIPTIONS_FORM, DEBUG_FORM_VALIDATION]):
53 log.warning("Debugging options enabled!")
56# =============================================================================
57# Widget resources
58# =============================================================================
60def get_head_form_html(req: "Request", forms: List[Form]) -> str:
61 """
62 Returns the extra HTML that needs to be injected into the ``<head>``
63 section for a Deform form to work properly.
64 """
65 # https://docs.pylonsproject.org/projects/deform/en/latest/widget.html#widget-requirements
66 js_resources = [] # type: List[str]
67 css_resources = [] # type: List[str]
68 for form in forms:
69 resources = form.get_widget_resources() # type: Dict[str, List[str]]
70 # Add, ignoring duplicates:
71 js_resources.extend(x for x in resources['js']
72 if x not in js_resources)
73 css_resources.extend(x for x in resources['css']
74 if x not in css_resources)
75 js_links = [req.static_url(r) for r in js_resources]
76 css_links = [req.static_url(r) for r in css_resources]
77 js_tags = ['<script type="text/javascript" src="%s"></script>' % link
78 for link in js_links]
79 css_tags = ['<link rel="stylesheet" href="%s"/>' % link
80 for link in css_links]
81 tags = js_tags + css_tags
82 head_html = "\n".join(tags)
83 return head_html
86# =============================================================================
87# Debugging form errors (which can be hidden in their depths)
88# =============================================================================
89# I'm not alone in the problem of errors from a HiddenWidget:
90# https://groups.google.com/forum/?fromgroups#!topic/pylons-discuss/LNHDq6KvNLI
91# https://groups.google.com/forum/#!topic/pylons-discuss/Lr1d1VpMycU
93class DeformErrorInterface(object):
94 """
95 Class to record information about Deform errors.
96 """
97 def __init__(self, msg: str, *children: "DeformErrorInterface") -> None:
98 """
99 Args:
100 msg: error message
101 children: further, child errors (e.g. from subfields with problems)
102 """
103 self._msg = msg
104 self.children = children
106 def __str__(self) -> str:
107 return self._msg
110class InformativeForm(Form):
111 """
112 A Deform form class that shows its errors.
113 """
114 def validate(self,
115 controls: Iterable[Tuple[str, str]],
116 subcontrol: str = None) -> Any:
117 """
118 Validates the form.
120 Args:
121 controls: an iterable of ``(key, value)`` tuples
122 subcontrol:
124 Returns:
125 a Colander ``appstruct``
127 Raises:
128 ValidationFailure: on failure
129 """
130 try:
131 return super().validate(controls, subcontrol)
132 except ValidationFailure as e:
133 if DEBUG_FORM_VALIDATION:
134 log.warning("Validation failure: {!r}; {}",
135 e, self._get_form_errors())
136 self._show_hidden_widgets_for_fields_with_errors(self)
137 raise
139 def _show_hidden_widgets_for_fields_with_errors(self,
140 field: Field) -> None:
141 if field.error:
142 widget = getattr(field, "widget", None)
143 # log.warning(repr(widget))
144 # log.warning(repr(widget.hidden))
145 if widget is not None and widget.hidden:
146 # log.critical("Found hidden widget for field with error!")
147 widget.hidden = False
148 for child_field in field.children:
149 self._show_hidden_widgets_for_fields_with_errors(child_field)
151 def _collect_error_errors(self,
152 errorlist: List[str],
153 error: DeformErrorInterface) -> None:
154 if error is None:
155 return
156 errorlist.append(str(error))
157 for child_error in error.children: # typically: subfields
158 self._collect_error_errors(errorlist, child_error)
160 def _collect_form_errors(self,
161 errorlist: List[str],
162 field: Field,
163 hidden_only: bool = False):
164 if hidden_only:
165 widget = getattr(field, "widget", None)
166 if not isinstance(widget, HiddenWidget):
167 return
168 # log.critical(repr(field))
169 self._collect_error_errors(errorlist, field.error)
170 for child_field in field.children:
171 self._collect_form_errors(errorlist, child_field,
172 hidden_only=hidden_only)
174 def _get_form_errors(self, hidden_only: bool = False) -> str:
175 errorlist = [] # type: List[str]
176 self._collect_form_errors(errorlist, self, hidden_only=hidden_only)
177 return "; ".join(repr(e) for e in errorlist)
180def debug_validator(validator: ValidatorType) -> ValidatorType:
181 """
182 Use as a wrapper around a validator, e.g.
184 .. code-block:: python
186 self.validator = debug_validator(OneOf(["some", "values"]))
188 If you do this, the log will show the thinking of the validator (what it's
189 trying to validate, and whether it accepted or rejected the value).
190 """
191 def _validate(node: SchemaNode, value: Any) -> None:
192 log.debug("Validating: {!r}", value)
193 try:
194 validator(node, value)
195 log.debug("... accepted")
196 except Invalid:
197 log.debug("... rejected")
198 raise
200 return _validate
203# =============================================================================
204# DynamicDescriptionsForm
205# =============================================================================
207def gen_fields(field: Field) -> Generator[Field, None, None]:
208 """
209 Starting with a Deform :class:`Field`, yield the field itself and any
210 children.
211 """
212 yield field
213 for c in field.children:
214 for f in gen_fields(c):
215 yield f
218class DynamicDescriptionsForm(InformativeForm):
219 """
220 For explanation, see
221 :class:`cardinal_pythonlib.colander_utils.ValidateDangerousOperationNode`.
223 In essence, this allows a schema to change its ``description`` properties
224 during form validation, and then to have them reflected in the form (which
225 won't happen with a standard Deform :class:`Form`, since it normally copies
226 its descriptions from its schema at creation time).
228 The upshot is that we can store temporary values in a form and validate
229 against them.
231 The use case is to generate a random string which the user has to enter to
232 confirm dangerous operations.
233 """
234 def __init__(self,
235 *args,
236 dynamic_descriptions: bool = True,
237 dynamic_titles: bool = False,
238 **kwargs) -> None:
239 """
240 Args:
241 args: other positional arguments to :class:`InformativeForm`
242 dynamic_descriptions: use dynamic descriptions?
243 dynamic_titles: use dynamic titles?
244 kwargs: other keyword arguments to :class:`InformativeForm`
245 """
246 self.dynamic_descriptions = dynamic_descriptions
247 self.dynamic_titles = dynamic_titles
248 super().__init__(*args, **kwargs)
250 def validate(self,
251 controls: Iterable[Tuple[str, str]],
252 subcontrol: str = None) -> Any:
253 try:
254 return super().validate(controls, subcontrol)
255 finally:
256 for f in gen_fields(self):
257 if self.dynamic_titles:
258 if DEBUG_DYNAMIC_DESCRIPTIONS_FORM:
259 log.debug("Rewriting title for {!r} from {!r} to {!r}",
260 f, f.title, f.schema.title)
261 f.title = f.schema.title
262 if self.dynamic_descriptions:
263 if DEBUG_DYNAMIC_DESCRIPTIONS_FORM:
264 log.debug(
265 "Rewriting description for {!r} from {!r} to {!r}",
266 f, f.description, f.schema.description)
267 f.description = f.schema.description