-
Notifications
You must be signed in to change notification settings - Fork 11
/
fields.py
362 lines (296 loc) · 10 KB
/
fields.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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# -*- coding: utf-8 -*-
import logging
import cgi
import widgets
from . import convs
from ..utils import cached_property
from ..utils.odict import OrderedDict
from .perms import FieldPerm
from .media import FormMedia
logger = logging.getLogger(__name__)
class BaseField(object):
'''
Simple container class which ancestors represents various parts of Form.
Encapsulates converter, various fields attributes, methods for data
access control
'''
#: :class:`FieldPerm` instance determining field's access permissions.
#: Can be set by field inheritance or throught constructor.
perm_getter = FieldPerm()
# defaults
conv = convs.Char
widget = widgets.TextInput()
label = None
media = FormMedia()
def __init__(self, name, conv=None, parent=None, **kwargs):
kwargs.update(dict(
parent=parent,
name=name,
conv=(conv or self.conv)(field=self),
widget=(kwargs.get('widget') or self.widget)(field=self),
))
self._init_kwargs = kwargs
self.__dict__.update(kwargs)
def __call__(self, **kwargs):
'''
Creates current object's copy with extra constructor arguments passed.
'''
params = dict(self._init_kwargs, **kwargs)
return self.__class__(**params)
@property
def multiple(self):
return self.conv.multiple
@property
def env(self):
return self.parent.env
@property
def form(self):
return self.parent.form
@property
def input_name(self):
'''
Name of field's input element generated in account to possible
nesting of fields. The input name is to be used in templates as value
of Input (Select, etc) element's Name attribute and Label element's For
attribute.
'''
return self.parent.prefix + self.name
@property
def error(self):
return self.form.errors.get(self.input_name)
@property
def clean_value(self):
'''
Current field's converted value from form's python_data.
'''
return self.parent.python_data[self.name]
@cached_property
def _relative_id(self): # XXX what is this?
return self.form.get_field_id(self)
@property
def id(self):
# We use template names in list to replace, so we must use it here to
# insure unique IDs.
return '%s-%s' % (self.form.id, self.input_name)
def from_python(self, value):
return self.conv.from_python(value)
@cached_property
def permissions(self):
'''
Returns field's access permissions
'''
return self.perm_getter.get_perms(self)
@cached_property
def writable(self):
return 'w' in self.permissions
@cached_property
def readable(self):
return 'r' in self.permissions
@property
def render_type(self):
return self.widget.render_type
def render(self):
return self.widget.render(self.raw_value)
def get_media(self):
media = FormMedia(self.media)
media += self.widget.get_media()
return media
class Field(BaseField):
'''
Atomic field
'''
#: :class:`Conv` subclass or instance used to convert field data
#: and validate it
conv = convs.Char
_null_value = ''
def get_initial(self):
if hasattr(self, 'initial'):
return self.initial
if self.multiple:
return []
return None
@property
def raw_value(self):
if self.multiple:
return self.form.raw_data.getall(self.input_name)
else:
return self.form.raw_data.get(self.input_name, '')
def set_raw_value(self, raw_data, value):
if self.multiple:
try:
del raw_data[self.input_name]
except KeyError:
pass
for v in value:
raw_data.add(self.input_name, v)
else:
raw_data[self.input_name] = value
def _check_value_type(self, values):
if not self.multiple:
values = [values]
for value in values:
if not isinstance(value, basestring):
self.form.errors[self.input_name] = 'Given value has incompatible type'
return False
return True
def accept(self):
value = self.raw_value
if not self._check_value_type(value):
value = [] if self.multiple else self._null_value
return self.conv.accept(value)
class AggregateField(BaseField):
@property
def python_data(self):
'''Representation of aggregate value as dictionary.'''
try:
value = self.clean_value
except LookupError:
value = self.get_initial()
return self.from_python(value)
class FieldSet(AggregateField):
'''
Container field aggregating a couple of other different fields
'''
template = 'widgets/fieldset'
render_type = 'default'
conv = convs.Converter
def __init__(self, name, conv=None, fields=[], **kwargs):
if kwargs.get('parent'):
conv = (conv or self.conv)(field=self)
fields = [field(parent=self) for field in fields]
kwargs.update(dict(
name=name,
conv=conv,
fields=fields,
))
BaseField.__init__(self, **kwargs)
@property
def prefix(self):
return self.input_name+'.'
def get_field(self, name):
names = name.split('.', 1)
for field in self.fields:
if field.name == names[0]:
if len(names) > 1:
return field.get_field(names[1])
return field
return None
def get_initial(self):
result = dict((field.name, field.get_initial())
for field in self.fields)
return self.conv.accept(result, silent=True)
def set_raw_value(self, raw_data, value):
# fills in raw_data multidict, resulting keys are field's absolute names
assert isinstance(value, dict), 'To set raw value need dict, got %r' % value
for field in self.fields:
subvalue = value[field.name]
field.set_raw_value(raw_data, field.from_python(subvalue))
def accept(self):
result = dict(self.python_data)
for field in self.fields:
if field.writable:
result[field.name] = field.accept()
else:
# readonly field
field.set_raw_value(self.form.raw_data,
field.from_python(result[field.name]))
return self.conv.accept(result)
def render(self):
return self.env.template.render(self.template, field=self)
def get_media(self):
media = BaseField.get_media(self)
for field in self.fields:
media += field.get_media()
return media
class FieldList(AggregateField):
'''
Container aggregating a couple of similar fields
'''
order = False
template = 'widgets/fieldlist'
render_type = 'default'
conv = convs.List
def __init__(self, name, conv=None, field=Field(None),
parent=None, **kwargs):
if parent:
conv = (conv or self.conv)(field=self)
field = field(parent=self)
kwargs.update(dict(
parent=parent,
name=name,
conv=conv,
field=field,
))
BaseField.__init__(self, **kwargs)
@property
def prefix(self):
return self.input_name+'-'
def get_initial(self):
return []
def get_field(self, name):
names = name.split('.', 1)
if self.field.name == names[0] or self.field.name is None:
if len(names) > 1:
return self.field.get_field(names[1])
return self.field
return None
@property
def indeces_input_name(self):
return self.input_name+'-indeces'
def accept(self):
old = self.python_data
result = OrderedDict()
for index in self.form.raw_data.getall(self.indeces_input_name):
try:
#XXX: we do not convert index to int, just check it.
# is it good idea?
int(index)
except ValueError:
logger.warning('Got incorrect index from form: %r', index)
continue
#TODO: describe this
field = self.field(name=str(index))
if not field.writable:
# readonly field
if index in old:
result[field.name] = old[field.name]
else:
result[field.name] = field.accept()
return self.conv.accept(result)
def set_raw_value(self, raw_data, value):
indeces = []
for index in range(1, len(value)+1):
index = str(index)
subvalue = value[index]
subfield = self.field(name=index)
subfield.set_raw_value(raw_data, subfield.from_python(subvalue))
indeces.append(index)
if self.indeces_input_name in self.form.raw_data:
del self.form.raw_data[self.indeces_input_name]
for index in indeces:
self.form.raw_data.add(self.indeces_input_name, index)
def render(self):
return self.env.template.render(self.template, field=self)
def get_media(self):
media = BaseField.get_media(self)
media += self.field.get_media()
return media
class FileField(Field):
'''
The simpliest file field
'''
_null_value = None
def get_default(self):
return None # XXX
def set_raw_value(self, raw_data, value):
pass
def _check_value_type(self, values):
if not self.multiple:
values = [values]
for value in values:
if value and \
not isinstance(value, cgi.FieldStorage) and \
not hasattr(value, 'read'): # XXX is this right?
self.form.errors[self.input_name] = 'Given value is not file'
return False
return True