-
Notifications
You must be signed in to change notification settings - Fork 35
/
__init__.py
292 lines (248 loc) · 8.06 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
try:
from collections.abc import Mapping, MutableMapping
except:
from collections import Mapping, MutableMapping
import six
from wtforms import Form
try:
from wtforms_sqlalchemy.fields import (
QuerySelectField,
QuerySelectMultipleField
)
HAS_SQLALCHEMY_SUPPORT = True
except ImportError:
try:
from wtforms.ext.sqlalchemy.fields import (
QuerySelectField,
QuerySelectMultipleField
)
HAS_SQLALCHEMY_SUPPORT = True
except ImportError:
HAS_SQLALCHEMY_SUPPORT = False
from wtforms.fields import (
_unset_value,
BooleanField,
Field,
FieldList,
FileField,
FormField,
StringField
)
from wtforms.validators import DataRequired, Optional
__version__ = '0.3.4'
class InvalidData(Exception):
pass
def flatten_json(
form,
json,
parent_key='',
separator='-',
skip_unknown_keys=True
):
"""Flattens given JSON dict to cope with WTForms dict structure.
:form form: WTForms Form object
:param json: json to be converted into flat WTForms style dict
:param parent_key: this argument is used internally be recursive calls
:param separator: default separator
:param skip_unknown_keys:
if True unknown keys will be skipped, if False throws InvalidData
exception whenever unknown key is encountered
Examples::
>>> flatten_json(MyForm, {'a': {'b': 'c'}})
{'a-b': 'c'}
"""
if not isinstance(json, Mapping):
raise InvalidData(
u'This function only accepts dict-like data structures.'
)
items = []
for key, value in json.items():
try:
unbound_field = getattr(form, key)
except AttributeError:
if skip_unknown_keys:
continue
else:
raise InvalidData(u"Unknown field name '%s'." % key)
try:
field_class = unbound_field.field_class
except AttributeError:
if skip_unknown_keys:
continue
else:
raise InvalidData(u"Key '%s' is not valid field class." % key)
new_key = parent_key + separator + key if parent_key else key
if isinstance(value, MutableMapping):
if issubclass(field_class, FormField):
nested_form_class = unbound_field.bind(Form(), '').form_class
items.extend(
flatten_json(nested_form_class, value, new_key)
.items()
)
else:
items.append((new_key, value))
elif isinstance(value, list):
if issubclass(field_class, FieldList):
nested_unbound_field = unbound_field.bind(
Form(),
''
).unbound_field
items.extend(
flatten_json_list(
nested_unbound_field,
value,
new_key,
separator
)
)
else:
items.append((new_key, value))
else:
items.append((new_key, value))
return dict(items)
def flatten_json_list(field, json, parent_key='', separator='-'):
items = []
for i, item in enumerate(json):
new_key = parent_key + separator + str(i)
if (
isinstance(item, dict) and
issubclass(getattr(field, 'field_class'), FormField)
):
nested_class = field.field_class(
*field.args,
**field.kwargs
).bind(Form(), '').form_class
items.extend(
flatten_json(nested_class, item, new_key)
.items()
)
else:
items.append((new_key, item))
return items
@property
def patch_data(self):
if hasattr(self, '_patch_data'):
return self._patch_data
data = {}
def is_optional(field):
return Optional in [v.__class__ for v in field.validators]
def is_required(field):
return DataRequired in [v.__class__ for v in field.validators]
for name, f in six.iteritems(self._fields):
if f.is_missing:
if is_optional(f):
continue
elif not is_required(f) and f.default is None:
continue
elif isinstance(f, FieldList) and f.min_entries == 0:
continue
if isinstance(f, FormField):
data[name] = f.patch_data
elif isinstance(f, FieldList):
if issubclass(f.unbound_field.field_class, FormField):
data[name] = [i.patch_data for i in f.entries]
else:
data[name] = [i.data for i in f.entries]
else:
data[name] = f.data
return data
def monkey_patch_field_process(func):
"""
Monkey patches Field.process method to better understand missing values.
"""
def process(self, formdata, data=_unset_value, **kwargs):
call_original_func = True
if not isinstance(self, FormField):
if formdata and self.name in formdata:
if (
len(formdata.getlist(self.name)) == 1 and
formdata.getlist(self.name) == [None]
):
call_original_func = False
self.data = None
self.is_missing = not bool(formdata.getlist(self.name))
else:
self.is_missing = True
if call_original_func:
func(self, formdata, data=data, **kwargs)
if (
formdata and self.name in formdata and
formdata.getlist(self.name) == [None] and
isinstance(self, FormField)
):
self.form._is_missing = False
self.form._patch_data = None
if isinstance(self, StringField) and not isinstance(self, FileField):
if not self.data:
try:
self.data = self.default()
except TypeError:
self.data = self.default
else:
self.data = six.text_type(self.data)
return process
class MultiDict(dict):
def getlist(self, key):
val = self[key]
if not isinstance(val, list):
val = [val]
return val
def getall(self, key):
return [self[key]]
@classmethod
def from_json(
cls,
formdata=None,
obj=None,
prefix='',
data=None,
meta=None,
skip_unknown_keys=True,
**kwargs
):
form = cls(
formdata=MultiDict(
flatten_json(cls, formdata, skip_unknown_keys=skip_unknown_keys)
) if formdata else None,
obj=obj,
prefix=prefix,
data=data,
meta=meta,
**kwargs
)
return form
@property
def is_missing(self):
if hasattr(self, '_is_missing'):
return self._is_missing
for name, field in self._fields.items():
if not field.is_missing:
return False
return True
@property
def field_list_is_missing(self):
if hasattr(self, '_is_missing'):
return self._is_missing
return all([field.is_missing for field in self.entries])
def monkey_patch_process_formdata(func):
def process_formdata(self, valuelist):
valuelist = list(map(six.text_type, valuelist))
return func(self, valuelist)
return process_formdata
def init():
Form.is_missing = is_missing
FieldList.is_missing = field_list_is_missing
Form.from_json = from_json
Form.patch_data = patch_data
FieldList.patch_data = patch_data
if HAS_SQLALCHEMY_SUPPORT:
QuerySelectField.process_formdata = monkey_patch_process_formdata(
QuerySelectField.process_formdata
)
QuerySelectMultipleField.process_formdata = \
monkey_patch_process_formdata(
QuerySelectMultipleField.process_formdata
)
Field.process = monkey_patch_field_process(Field.process)
FormField.process = monkey_patch_field_process(FormField.process)
BooleanField.false_values = BooleanField.false_values + (False,)