Hide keyboard shortcuts

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 

3 

4""" 

5=============================================================================== 

6 

7 Original code copyright (C) 2009-2021 Rudolf Cardinal (rudolf@pobox.com). 

8 

9 This file is part of cardinal_pythonlib. 

10 

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 

14 

15 https://www.apache.org/licenses/LICENSE-2.0 

16 

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. 

22 

23=============================================================================== 

24""" 

25 

26from typing import (Any, Callable, Dict, Generator, Iterable, List, Tuple, 

27 TYPE_CHECKING) 

28 

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 

36 

37if TYPE_CHECKING: 

38 # noinspection PyUnresolvedReferences 

39 from pyramid.request import Request 

40 

41log = get_brace_style_log_with_null_handler(__name__) 

42 

43ValidatorType = Callable[[SchemaNode, Any], None] # called as v(node, value) 

44 

45# ============================================================================= 

46# Debugging options 

47# ============================================================================= 

48 

49DEBUG_DYNAMIC_DESCRIPTIONS_FORM = False 

50DEBUG_FORM_VALIDATION = False 

51 

52if any([DEBUG_DYNAMIC_DESCRIPTIONS_FORM, DEBUG_FORM_VALIDATION]): 

53 log.warning("Debugging options enabled!") 

54 

55 

56# ============================================================================= 

57# Widget resources 

58# ============================================================================= 

59 

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 

84 

85 

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 

92 

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 

105 

106 def __str__(self) -> str: 

107 return self._msg 

108 

109 

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. 

119 

120 Args: 

121 controls: an iterable of ``(key, value)`` tuples 

122 subcontrol: 

123 

124 Returns: 

125 a Colander ``appstruct`` 

126 

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 

138 

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) 

150 

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) 

159 

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) 

173 

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) 

178 

179 

180def debug_validator(validator: ValidatorType) -> ValidatorType: 

181 """ 

182 Use as a wrapper around a validator, e.g. 

183 

184 .. code-block:: python 

185 

186 self.validator = debug_validator(OneOf(["some", "values"])) 

187 

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 

199 

200 return _validate 

201 

202 

203# ============================================================================= 

204# DynamicDescriptionsForm 

205# ============================================================================= 

206 

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 

216 

217 

218class DynamicDescriptionsForm(InformativeForm): 

219 """ 

220 For explanation, see 

221 :class:`cardinal_pythonlib.colander_utils.ValidateDangerousOperationNode`. 

222 

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). 

227 

228 The upshot is that we can store temporary values in a form and validate 

229 against them. 

230 

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) 

249 

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