|
1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """ |
|
19 The Field class and basic fields |
|
20 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
21 |
|
22 .. Note:: |
|
23 Fields are used to control what's edited in forms. They makes the link between |
|
24 something to edit and its display in the form. Actual display is handled by a |
|
25 widget associated to the field. |
|
26 |
|
27 Let first see the base class for fields: |
|
28 |
|
29 .. autoclass:: cubicweb.web.formfields.Field |
|
30 |
|
31 Now, you usually don't use that class but one of the concrete field classes |
|
32 described below, according to what you want to edit. |
|
33 |
|
34 Basic fields |
|
35 '''''''''''' |
|
36 |
|
37 .. autoclass:: cubicweb.web.formfields.StringField() |
|
38 .. autoclass:: cubicweb.web.formfields.PasswordField() |
|
39 .. autoclass:: cubicweb.web.formfields.IntField() |
|
40 .. autoclass:: cubicweb.web.formfields.BigIntField() |
|
41 .. autoclass:: cubicweb.web.formfields.FloatField() |
|
42 .. autoclass:: cubicweb.web.formfields.BooleanField() |
|
43 .. autoclass:: cubicweb.web.formfields.DateField() |
|
44 .. autoclass:: cubicweb.web.formfields.DateTimeField() |
|
45 .. autoclass:: cubicweb.web.formfields.TimeField() |
|
46 .. autoclass:: cubicweb.web.formfields.TimeIntervalField() |
|
47 |
|
48 Compound fields |
|
49 '''''''''''''''' |
|
50 |
|
51 .. autoclass:: cubicweb.web.formfields.RichTextField() |
|
52 .. autoclass:: cubicweb.web.formfields.FileField() |
|
53 .. autoclass:: cubicweb.web.formfields.CompoundField() |
|
54 |
|
55 .. autoclass cubicweb.web.formfields.EditableFileField() XXX should be a widget |
|
56 |
|
57 Entity specific fields and function |
|
58 ''''''''''''''''''''''''''''''''''' |
|
59 |
|
60 .. autoclass:: cubicweb.web.formfields.RelationField() |
|
61 .. autofunction:: cubicweb.web.formfields.guess_field |
|
62 |
|
63 """ |
|
64 __docformat__ = "restructuredtext en" |
|
65 |
|
66 from warnings import warn |
|
67 from datetime import datetime, timedelta |
|
68 |
|
69 from six import PY2, text_type, string_types |
|
70 |
|
71 from logilab.mtconverter import xml_escape |
|
72 from logilab.common import nullobject |
|
73 from logilab.common.date import ustrftime |
|
74 from logilab.common.configuration import format_time |
|
75 from logilab.common.textutils import apply_units, TIME_UNITS |
|
76 |
|
77 from yams.schema import KNOWN_METAATTRIBUTES, role_name |
|
78 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint, |
|
79 FormatConstraint) |
|
80 |
|
81 from cubicweb import Binary, tags, uilib |
|
82 from cubicweb.utils import support_args |
|
83 from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \ |
|
84 formwidgets as fw |
|
85 from cubicweb.web.views import uicfg |
|
86 |
|
87 class UnmodifiedField(Exception): |
|
88 """raise this when a field has not actually been edited and you want to skip |
|
89 it |
|
90 """ |
|
91 |
|
92 def normalize_filename(filename): |
|
93 return filename.split('\\')[-1] |
|
94 |
|
95 def vocab_sort(vocab): |
|
96 """sort vocabulary, considering option groups""" |
|
97 result = [] |
|
98 partresult = [] |
|
99 for label, value in vocab: |
|
100 if value is None: # opt group start |
|
101 if partresult: |
|
102 result += sorted(partresult) |
|
103 partresult = [] |
|
104 result.append( (label, value) ) |
|
105 else: |
|
106 partresult.append( (label, value) ) |
|
107 result += sorted(partresult) |
|
108 return result |
|
109 |
|
110 _MARKER = nullobject() |
|
111 |
|
112 class Field(object): |
|
113 """This class is the abstract base class for all fields. It hold a bunch |
|
114 of attributes which may be used for fine control of the behaviour of a |
|
115 concrete field. |
|
116 |
|
117 **Attributes** |
|
118 |
|
119 All the attributes described below have sensible default value which may be |
|
120 overriden by named arguments given to field's constructor. |
|
121 |
|
122 :attr:`name` |
|
123 base name of the field (basestring). The actual input name is returned by |
|
124 the :meth:`input_name` method and may differ from that name (for instance |
|
125 if `eidparam` is true). |
|
126 :attr:`id` |
|
127 DOM identifier (default to the same value as `name`), should be unique in |
|
128 a form. |
|
129 :attr:`label` |
|
130 label of the field (default to the same value as `name`). |
|
131 :attr:`help` |
|
132 help message about this field. |
|
133 :attr:`widget` |
|
134 widget associated to the field. Each field class has a default widget |
|
135 class which may be overriden per instance. |
|
136 :attr:`value` |
|
137 field value. May be an actual value or a callable which should take the |
|
138 form as argument and return a value. |
|
139 :attr:`choices` |
|
140 static vocabulary for this field. May be a list of values, a list of |
|
141 (label, value) tuples or a callable which should take the form and field |
|
142 as arguments and return a list of values or a list of (label, value). |
|
143 :attr:`required` |
|
144 bool flag telling if the field is required or not. |
|
145 :attr:`sort` |
|
146 bool flag telling if the vocabulary (either static vocabulary specified |
|
147 in `choices` or dynamic vocabulary fetched from the form) should be |
|
148 sorted on label. |
|
149 :attr:`internationalizable` |
|
150 bool flag telling if the vocabulary labels should be translated using the |
|
151 current request language. |
|
152 :attr:`eidparam` |
|
153 bool flag telling if this field is linked to a specific entity |
|
154 :attr:`role` |
|
155 when the field is linked to an entity attribute or relation, tells the |
|
156 role of the entity in the relation (eg 'subject' or 'object'). If this is |
|
157 not an attribute or relation of the edited entity, `role` should be |
|
158 `None`. |
|
159 :attr:`fieldset` |
|
160 optional fieldset to which this field belongs to |
|
161 :attr:`order` |
|
162 key used by automatic forms to sort fields |
|
163 :attr:`ignore_req_params` |
|
164 when true, this field won't consider value potentially specified using |
|
165 request's form parameters (eg you won't be able to specify a value using for |
|
166 instance url like http://mywebsite.com/form?field=value) |
|
167 |
|
168 .. currentmodule:: cubicweb.web.formfields |
|
169 |
|
170 **Generic methods** |
|
171 |
|
172 .. automethod:: Field.input_name |
|
173 .. automethod:: Field.dom_id |
|
174 .. automethod:: Field.actual_fields |
|
175 |
|
176 **Form generation methods** |
|
177 |
|
178 .. automethod:: form_init |
|
179 .. automethod:: typed_value |
|
180 |
|
181 **Post handling methods** |
|
182 |
|
183 .. automethod:: process_posted |
|
184 .. automethod:: process_form_value |
|
185 |
|
186 """ |
|
187 # default widget associated to this class of fields. May be overriden per |
|
188 # instance |
|
189 widget = fw.TextInput |
|
190 # does this field requires a multipart form |
|
191 needs_multipart = False |
|
192 # class attribute used for ordering of fields in a form |
|
193 __creation_rank = 0 |
|
194 |
|
195 eidparam = False |
|
196 role = None |
|
197 id = None |
|
198 help = None |
|
199 required = False |
|
200 choices = None |
|
201 sort = True |
|
202 internationalizable = False |
|
203 fieldset = None |
|
204 order = None |
|
205 value = _MARKER |
|
206 fallback_on_none_attribute = False |
|
207 ignore_req_params = False |
|
208 |
|
209 def __init__(self, name=None, label=_MARKER, widget=None, **kwargs): |
|
210 for key, val in kwargs.items(): |
|
211 assert hasattr(self.__class__, key) and not key[0] == '_', key |
|
212 setattr(self, key, val) |
|
213 self.name = name |
|
214 if label is _MARKER: |
|
215 label = name or _MARKER |
|
216 self.label = label |
|
217 # has to be done after other attributes initialization |
|
218 self.init_widget(widget) |
|
219 # ordering number for this field instance |
|
220 self.creation_rank = Field.__creation_rank |
|
221 Field.__creation_rank += 1 |
|
222 |
|
223 def as_string(self, repr=True): |
|
224 l = [u'<%s' % self.__class__.__name__] |
|
225 for attr in ('name', 'eidparam', 'role', 'id', 'value'): |
|
226 value = getattr(self, attr) |
|
227 if value is not None and value is not _MARKER: |
|
228 l.append('%s=%r' % (attr, value)) |
|
229 if repr: |
|
230 l.append('@%#x' % id(self)) |
|
231 return u'%s>' % ' '.join(l) |
|
232 |
|
233 def __unicode__(self): |
|
234 return self.as_string(False) |
|
235 |
|
236 if PY2: |
|
237 def __str__(self): |
|
238 return self.as_string(False).encode('UTF8') |
|
239 else: |
|
240 __str__ = __unicode__ |
|
241 |
|
242 def __repr__(self): |
|
243 return self.as_string(True) |
|
244 |
|
245 def init_widget(self, widget): |
|
246 if widget is not None: |
|
247 self.widget = widget |
|
248 elif self.choices and not self.widget.vocabulary_widget: |
|
249 self.widget = fw.Select() |
|
250 if isinstance(self.widget, type): |
|
251 self.widget = self.widget() |
|
252 |
|
253 def set_name(self, name): |
|
254 """automatically set .label when name is set""" |
|
255 assert name |
|
256 self.name = name |
|
257 if self.label is _MARKER: |
|
258 self.label = name |
|
259 |
|
260 def is_visible(self): |
|
261 """return true if the field is not an hidden field""" |
|
262 return not isinstance(self.widget, fw.HiddenInput) |
|
263 |
|
264 def actual_fields(self, form): |
|
265 """Fields may be composed of other fields. For instance the |
|
266 :class:`~cubicweb.web.formfields.RichTextField` is containing a format |
|
267 field to define the text format. This method returns actual fields that |
|
268 should be considered for display / edition. It usually simply return |
|
269 self. |
|
270 """ |
|
271 yield self |
|
272 |
|
273 def format_value(self, req, value): |
|
274 """return value suitable for display where value may be a list or tuple |
|
275 of values |
|
276 """ |
|
277 if isinstance(value, (list, tuple)): |
|
278 return [self.format_single_value(req, val) for val in value] |
|
279 return self.format_single_value(req, value) |
|
280 |
|
281 def format_single_value(self, req, value): |
|
282 """return value suitable for display""" |
|
283 if value is None or value is False: |
|
284 return u'' |
|
285 if value is True: |
|
286 return u'1' |
|
287 return text_type(value) |
|
288 |
|
289 def get_widget(self, form): |
|
290 """return the widget instance associated to this field""" |
|
291 return self.widget |
|
292 |
|
293 def input_name(self, form, suffix=None): |
|
294 """Return the 'qualified name' for this field, e.g. something suitable |
|
295 to use as HTML input name. You can specify a suffix that will be |
|
296 included in the name when widget needs several inputs. |
|
297 """ |
|
298 # caching is necessary else we get some pb on entity creation : |
|
299 # entity.eid is modified from creation mark (eg 'X') to its actual eid |
|
300 # (eg 123), and then `field.input_name()` won't return the right key |
|
301 # anymore if not cached (first call to input_name done *before* eventual |
|
302 # eid affectation). |
|
303 # |
|
304 # note that you should NOT use @cached else it will create a memory leak |
|
305 # on persistent fields (eg created once for all on a form class) because |
|
306 # of the 'form' appobject argument: the cache will keep growing as new |
|
307 # form are created... |
|
308 try: |
|
309 return form.formvalues[(self, 'input_name', suffix)] |
|
310 except KeyError: |
|
311 name = self.role_name() |
|
312 if suffix is not None: |
|
313 name += suffix |
|
314 if self.eidparam: |
|
315 name = eid_param(name, form.edited_entity.eid) |
|
316 form.formvalues[(self, 'input_name', suffix)] = name |
|
317 return name |
|
318 |
|
319 def role_name(self): |
|
320 """return <field.name>-<field.role> if role is specified, else field.name""" |
|
321 assert self.name, 'field without a name (give it to constructor for explicitly built fields)' |
|
322 if self.role is not None: |
|
323 return role_name(self.name, self.role) |
|
324 return self.name |
|
325 |
|
326 def dom_id(self, form, suffix=None): |
|
327 """Return the HTML DOM identifier for this field, e.g. something |
|
328 suitable to use as HTML input id. You can specify a suffix that will be |
|
329 included in the name when widget needs several inputs. |
|
330 """ |
|
331 id = self.id or self.role_name() |
|
332 if suffix is not None: |
|
333 id += suffix |
|
334 if self.eidparam: |
|
335 return eid_param(id, form.edited_entity.eid) |
|
336 return id |
|
337 |
|
338 def typed_value(self, form, load_bytes=False): |
|
339 """Return the correctly typed value for this field in the form context. |
|
340 """ |
|
341 if self.eidparam and self.role is not None: |
|
342 entity = form.edited_entity |
|
343 if form._cw.vreg.schema.rschema(self.name).final: |
|
344 if entity.has_eid() or self.name in entity.cw_attr_cache: |
|
345 value = getattr(entity, self.name) |
|
346 if value is not None or not self.fallback_on_none_attribute: |
|
347 return value |
|
348 elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role): |
|
349 value = [r[0] for r in entity.related(self.name, self.role)] |
|
350 if value or not self.fallback_on_none_attribute: |
|
351 return value |
|
352 return self.initial_typed_value(form, load_bytes) |
|
353 |
|
354 def initial_typed_value(self, form, load_bytes): |
|
355 if self.value is not _MARKER: |
|
356 if callable(self.value): |
|
357 return self.value(form, self) |
|
358 return self.value |
|
359 formattr = '%s_%s_default' % (self.role, self.name) |
|
360 if self.eidparam and self.role is not None: |
|
361 if form._cw.vreg.schema.rschema(self.name).final: |
|
362 return form.edited_entity.e_schema.default(self.name) |
|
363 return form.linked_to.get((self.name, self.role), ()) |
|
364 return None |
|
365 |
|
366 def example_format(self, req): |
|
367 """return a sample string describing what can be given as input for this |
|
368 field |
|
369 """ |
|
370 return u'' |
|
371 |
|
372 def render(self, form, renderer): |
|
373 """render this field, which is part of form, using the given form |
|
374 renderer |
|
375 """ |
|
376 widget = self.get_widget(form) |
|
377 return widget.render(form, self, renderer) |
|
378 |
|
379 def vocabulary(self, form, **kwargs): |
|
380 """return vocabulary for this field. This method will be |
|
381 called by widgets which requires a vocabulary. |
|
382 |
|
383 It should return a list of tuple (label, value), where value |
|
384 *must be a unicode string*, not a typed value. |
|
385 """ |
|
386 assert self.choices is not None |
|
387 if callable(self.choices): |
|
388 # pylint: disable=E1102 |
|
389 if getattr(self.choices, '__self__', None) is self: |
|
390 vocab = self.choices(form=form, **kwargs) |
|
391 else: |
|
392 vocab = self.choices(form=form, field=self, **kwargs) |
|
393 else: |
|
394 vocab = self.choices |
|
395 if vocab and not isinstance(vocab[0], (list, tuple)): |
|
396 vocab = [(x, x) for x in vocab] |
|
397 if self.internationalizable: |
|
398 # the short-cirtcuit 'and' boolean operator is used here |
|
399 # to permit a valid empty string in vocabulary without |
|
400 # attempting to translate it by gettext (which can lead to |
|
401 # weird strings display) |
|
402 vocab = [(label and form._cw._(label), value) |
|
403 for label, value in vocab] |
|
404 if self.sort: |
|
405 vocab = vocab_sort(vocab) |
|
406 return vocab |
|
407 |
|
408 # support field as argument to avoid warning when used as format field value |
|
409 # callback |
|
410 def format(self, form, field=None): |
|
411 """return MIME type used for the given (text or bytes) field""" |
|
412 if self.eidparam and self.role == 'subject': |
|
413 entity = form.edited_entity |
|
414 if entity.e_schema.has_metadata(self.name, 'format') and ( |
|
415 entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache): |
|
416 return form.edited_entity.cw_attr_metadata(self.name, 'format') |
|
417 return form._cw.property_value('ui.default-text-format') |
|
418 |
|
419 def encoding(self, form): |
|
420 """return encoding used for the given (text) field""" |
|
421 if self.eidparam: |
|
422 entity = form.edited_entity |
|
423 if entity.e_schema.has_metadata(self.name, 'encoding') and ( |
|
424 entity.has_eid() or '%s_encoding' % self.name in entity): |
|
425 return form.edited_entity.cw_attr_metadata(self.name, 'encoding') |
|
426 return form._cw.encoding |
|
427 |
|
428 def form_init(self, form): |
|
429 """Method called at form initialization to trigger potential field |
|
430 initialization requiring the form instance. Do nothing by default. |
|
431 """ |
|
432 pass |
|
433 |
|
434 def has_been_modified(self, form): |
|
435 for field in self.actual_fields(form): |
|
436 if field._has_been_modified(form): |
|
437 return True # XXX |
|
438 return False # not modified |
|
439 |
|
440 def _has_been_modified(self, form): |
|
441 # fields not corresponding to an entity attribute / relations |
|
442 # are considered modified |
|
443 if not self.eidparam or not self.role or not form.edited_entity.has_eid(): |
|
444 return True # XXX |
|
445 try: |
|
446 if self.role == 'subject': |
|
447 previous_value = getattr(form.edited_entity, self.name) |
|
448 else: |
|
449 previous_value = getattr(form.edited_entity, |
|
450 'reverse_%s' % self.name) |
|
451 except AttributeError: |
|
452 # fields with eidparam=True but not corresponding to an actual |
|
453 # attribute or relation |
|
454 return True |
|
455 # if it's a non final relation, we need the eids |
|
456 if isinstance(previous_value, (list, tuple)): |
|
457 # widget should return a set of untyped eids |
|
458 previous_value = set(e.eid for e in previous_value) |
|
459 try: |
|
460 new_value = self.process_form_value(form) |
|
461 except ProcessFormError: |
|
462 return True |
|
463 except UnmodifiedField: |
|
464 return False # not modified |
|
465 if previous_value == new_value: |
|
466 return False # not modified |
|
467 return True |
|
468 |
|
469 def process_form_value(self, form): |
|
470 """Return the correctly typed value posted for this field.""" |
|
471 try: |
|
472 return form.formvalues[(self, form)] |
|
473 except KeyError: |
|
474 value = form.formvalues[(self, form)] = self._process_form_value(form) |
|
475 return value |
|
476 |
|
477 def _process_form_value(self, form): |
|
478 widget = self.get_widget(form) |
|
479 value = widget.process_field_data(form, self) |
|
480 return self._ensure_correctly_typed(form, value) |
|
481 |
|
482 def _ensure_correctly_typed(self, form, value): |
|
483 """widget might to return date as a correctly formatted string or as |
|
484 correctly typed objects, but process_for_value must return a typed value. |
|
485 Override this method to type the value if necessary |
|
486 """ |
|
487 return value or None |
|
488 |
|
489 def process_posted(self, form): |
|
490 """Return an iterator on (field, value) that has been posted for |
|
491 field returned by :meth:`~cubicweb.web.formfields.Field.actual_fields`. |
|
492 """ |
|
493 for field in self.actual_fields(form): |
|
494 if field is self: |
|
495 try: |
|
496 value = field.process_form_value(form) |
|
497 if field.no_value(value) and field.required: |
|
498 raise ProcessFormError(form._cw._("required field")) |
|
499 yield field, value |
|
500 except UnmodifiedField: |
|
501 continue |
|
502 else: |
|
503 # recursive function: we might have compound fields |
|
504 # of compound fields (of compound fields of ...) |
|
505 for field, value in field.process_posted(form): |
|
506 yield field, value |
|
507 |
|
508 @staticmethod |
|
509 def no_value(value): |
|
510 """return True if the value can be considered as no value for the field""" |
|
511 return value is None |
|
512 |
|
513 |
|
514 class StringField(Field): |
|
515 """Use this field to edit unicode string (`String` yams type). This field |
|
516 additionally support a `max_length` attribute that specify a maximum size for |
|
517 the string (`None` meaning no limit). |
|
518 |
|
519 Unless explicitly specified, the widget for this field will be: |
|
520 |
|
521 * :class:`~cubicweb.web.formwidgets.Select` if some vocabulary is specified |
|
522 using `choices` attribute |
|
523 |
|
524 * :class:`~cubicweb.web.formwidgets.TextInput` if maximum size is specified |
|
525 using `max_length` attribute and this length is inferior to 257. |
|
526 |
|
527 * :class:`~cubicweb.web.formwidgets.TextArea` in all other cases |
|
528 """ |
|
529 widget = fw.TextArea |
|
530 size = 45 |
|
531 placeholder = None |
|
532 |
|
533 def __init__(self, name=None, max_length=None, **kwargs): |
|
534 self.max_length = max_length # must be set before super call |
|
535 super(StringField, self).__init__(name=name, **kwargs) |
|
536 |
|
537 def init_widget(self, widget): |
|
538 if widget is None: |
|
539 if self.choices: |
|
540 widget = fw.Select() |
|
541 elif self.max_length and self.max_length < 257: |
|
542 widget = fw.TextInput() |
|
543 |
|
544 super(StringField, self).init_widget(widget) |
|
545 if isinstance(self.widget, fw.TextArea): |
|
546 self.init_text_area(self.widget) |
|
547 elif isinstance(self.widget, fw.TextInput): |
|
548 self.init_text_input(self.widget) |
|
549 |
|
550 if self.placeholder: |
|
551 self.widget.attrs.setdefault('placeholder', self.placeholder) |
|
552 |
|
553 def init_text_input(self, widget): |
|
554 if self.max_length: |
|
555 widget.attrs.setdefault('size', min(self.size, self.max_length)) |
|
556 widget.attrs.setdefault('maxlength', self.max_length) |
|
557 |
|
558 def init_text_area(self, widget): |
|
559 if self.max_length and self.max_length < 513: |
|
560 widget.attrs.setdefault('cols', 60) |
|
561 widget.attrs.setdefault('rows', 5) |
|
562 |
|
563 def set_placeholder(self, placeholder): |
|
564 self.placeholder = placeholder |
|
565 if self.widget and self.placeholder: |
|
566 self.widget.attrs.setdefault('placeholder', self.placeholder) |
|
567 |
|
568 |
|
569 class PasswordField(StringField): |
|
570 """Use this field to edit password (`Password` yams type, encoded python |
|
571 string). |
|
572 |
|
573 Unless explicitly specified, the widget for this field will be |
|
574 a :class:`~cubicweb.web.formwidgets.PasswordInput`. |
|
575 """ |
|
576 widget = fw.PasswordInput |
|
577 def form_init(self, form): |
|
578 if self.eidparam and form.edited_entity.has_eid(): |
|
579 # see below: value is probably set but we can't retreive it. Ensure |
|
580 # the field isn't show as a required field on modification |
|
581 self.required = False |
|
582 |
|
583 def typed_value(self, form, load_bytes=False): |
|
584 if self.eidparam: |
|
585 # no way to fetch actual password value with cw |
|
586 if form.edited_entity.has_eid(): |
|
587 return '' |
|
588 return self.initial_typed_value(form, load_bytes) |
|
589 return super(PasswordField, self).typed_value(form, load_bytes) |
|
590 |
|
591 |
|
592 class RichTextField(StringField): |
|
593 """This compound field allow edition of text (unicode string) in |
|
594 a particular format. It has an inner field holding the text format, |
|
595 that can be specified using `format_field` argument. If not specified |
|
596 one will be automaticall generated. |
|
597 |
|
598 Unless explicitly specified, the widget for this field will be a |
|
599 :class:`~cubicweb.web.formwidgets.FCKEditor` or a |
|
600 :class:`~cubicweb.web.formwidgets.TextArea`. according to the field's |
|
601 format and to user's preferences. |
|
602 """ |
|
603 |
|
604 widget = None |
|
605 def __init__(self, format_field=None, **kwargs): |
|
606 super(RichTextField, self).__init__(**kwargs) |
|
607 self.format_field = format_field |
|
608 |
|
609 def init_text_area(self, widget): |
|
610 pass |
|
611 |
|
612 def get_widget(self, form): |
|
613 if self.widget is None: |
|
614 if self.use_fckeditor(form): |
|
615 return fw.FCKEditor() |
|
616 widget = fw.TextArea() |
|
617 self.init_text_area(widget) |
|
618 return widget |
|
619 return self.widget |
|
620 |
|
621 def get_format_field(self, form): |
|
622 if self.format_field: |
|
623 return self.format_field |
|
624 # we have to cache generated field since it's use as key in the |
|
625 # context dictionary |
|
626 req = form._cw |
|
627 try: |
|
628 return req.data[self] |
|
629 except KeyError: |
|
630 fkwargs = {'eidparam': self.eidparam, 'role': self.role} |
|
631 if self.use_fckeditor(form): |
|
632 # if fckeditor is used and format field isn't explicitly |
|
633 # deactivated, we want an hidden field for the format |
|
634 fkwargs['widget'] = fw.HiddenInput() |
|
635 fkwargs['value'] = 'text/html' |
|
636 else: |
|
637 # else we want a format selector |
|
638 fkwargs['widget'] = fw.Select() |
|
639 fcstr = FormatConstraint() |
|
640 fkwargs['choices'] = fcstr.vocabulary(form=form) |
|
641 fkwargs['internationalizable'] = True |
|
642 fkwargs['value'] = self.format |
|
643 fkwargs['eidparam'] = self.eidparam |
|
644 field = StringField(name=self.name + '_format', **fkwargs) |
|
645 req.data[self] = field |
|
646 return field |
|
647 |
|
648 def actual_fields(self, form): |
|
649 yield self |
|
650 format_field = self.get_format_field(form) |
|
651 if format_field: |
|
652 yield format_field |
|
653 |
|
654 def use_fckeditor(self, form): |
|
655 """return True if fckeditor should be used to edit entity's attribute named |
|
656 `attr`, according to user preferences |
|
657 """ |
|
658 if form._cw.use_fckeditor(): |
|
659 return self.format(form) == 'text/html' |
|
660 return False |
|
661 |
|
662 def render(self, form, renderer): |
|
663 format_field = self.get_format_field(form) |
|
664 if format_field: |
|
665 # XXX we want both fields to remain vertically aligned |
|
666 if format_field.is_visible(): |
|
667 format_field.widget.attrs['style'] = 'display: block' |
|
668 result = format_field.render(form, renderer) |
|
669 else: |
|
670 result = u'' |
|
671 return result + self.get_widget(form).render(form, self, renderer) |
|
672 |
|
673 |
|
674 class FileField(StringField): |
|
675 """This compound field allow edition of binary stream (`Bytes` yams |
|
676 type). Three inner fields may be specified: |
|
677 |
|
678 * `format_field`, holding the file's format. |
|
679 * `encoding_field`, holding the file's content encoding. |
|
680 * `name_field`, holding the file's name. |
|
681 |
|
682 Unless explicitly specified, the widget for this field will be a |
|
683 :class:`~cubicweb.web.formwidgets.FileInput`. Inner fields, if any, |
|
684 will be added to a drop down menu at the right of the file input. |
|
685 """ |
|
686 widget = fw.FileInput |
|
687 needs_multipart = True |
|
688 |
|
689 def __init__(self, format_field=None, encoding_field=None, name_field=None, |
|
690 **kwargs): |
|
691 super(FileField, self).__init__(**kwargs) |
|
692 self.format_field = format_field |
|
693 self.encoding_field = encoding_field |
|
694 self.name_field = name_field |
|
695 |
|
696 def actual_fields(self, form): |
|
697 yield self |
|
698 if self.format_field: |
|
699 yield self.format_field |
|
700 if self.encoding_field: |
|
701 yield self.encoding_field |
|
702 if self.name_field: |
|
703 yield self.name_field |
|
704 |
|
705 def typed_value(self, form, load_bytes=False): |
|
706 if self.eidparam and self.role is not None: |
|
707 if form.edited_entity.has_eid(): |
|
708 if load_bytes: |
|
709 return getattr(form.edited_entity, self.name) |
|
710 # don't actually load data |
|
711 # XXX value should reflect if some file is already attached |
|
712 # * try to display name metadata |
|
713 # * check length(data) / data != null |
|
714 return True |
|
715 return False |
|
716 return super(FileField, self).typed_value(form, load_bytes) |
|
717 |
|
718 def render(self, form, renderer): |
|
719 wdgs = [self.get_widget(form).render(form, self, renderer)] |
|
720 if self.format_field or self.encoding_field: |
|
721 divid = '%s-advanced' % self.input_name(form) |
|
722 wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' % |
|
723 (xml_escape(uilib.toggle_action(divid)), |
|
724 form._cw._('show advanced fields'), |
|
725 xml_escape(form._cw.data_url('puce_down.png')), |
|
726 form._cw._('show advanced fields'))) |
|
727 wdgs.append(u'<div id="%s" class="hidden">' % divid) |
|
728 if self.name_field: |
|
729 wdgs.append(self.render_subfield(form, self.name_field, renderer)) |
|
730 if self.format_field: |
|
731 wdgs.append(self.render_subfield(form, self.format_field, renderer)) |
|
732 if self.encoding_field: |
|
733 wdgs.append(self.render_subfield(form, self.encoding_field, renderer)) |
|
734 wdgs.append(u'</div>') |
|
735 if not self.required and self.typed_value(form): |
|
736 # trick to be able to delete an uploaded file |
|
737 wdgs.append(u'<br/>') |
|
738 wdgs.append(tags.input(name=self.input_name(form, u'__detach'), |
|
739 type=u'checkbox')) |
|
740 wdgs.append(form._cw._('detach attached file')) |
|
741 return u'\n'.join(wdgs) |
|
742 |
|
743 def render_subfield(self, form, field, renderer): |
|
744 return (renderer.render_label(form, field) |
|
745 + field.render(form, renderer) |
|
746 + renderer.render_help(form, field) |
|
747 + u'<br/>') |
|
748 |
|
749 def _process_form_value(self, form): |
|
750 posted = form._cw.form |
|
751 if self.input_name(form, u'__detach') in posted: |
|
752 # drop current file value on explictily asked to detach |
|
753 return None |
|
754 try: |
|
755 value = posted[self.input_name(form)] |
|
756 except KeyError: |
|
757 # raise UnmodifiedField instead of returning None, since the later |
|
758 # will try to remove already attached file if any |
|
759 raise UnmodifiedField() |
|
760 # value is a 2-uple (filename, stream) or a list of such |
|
761 # tuples (multiple files) |
|
762 try: |
|
763 if isinstance(value, list): |
|
764 value = value[0] |
|
765 form.warning('mutiple files provided, however ' |
|
766 'only the first will be picked') |
|
767 filename, stream = value |
|
768 except ValueError: |
|
769 raise UnmodifiedField() |
|
770 # XXX avoid in memory loading of posted files. Requires Binary handling changes... |
|
771 value = Binary(stream.read()) |
|
772 if not value.getvalue(): # usually an unexistant file |
|
773 value = None |
|
774 else: |
|
775 # set filename on the Binary instance, may be used later in hooks |
|
776 value.filename = normalize_filename(filename) |
|
777 return value |
|
778 |
|
779 |
|
780 # XXX turn into a widget |
|
781 class EditableFileField(FileField): |
|
782 """This compound field allow edition of binary stream as |
|
783 :class:`~cubicweb.web.formfields.FileField` but expect that stream to |
|
784 actually contains some text. |
|
785 |
|
786 If the stream format is one of text/plain, text/html, text/rest, |
|
787 text/markdown |
|
788 then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionally |
|
789 displayed, allowing to directly the file's content when desired, instead |
|
790 of choosing a file from user's file system. |
|
791 """ |
|
792 editable_formats = ( |
|
793 'text/plain', 'text/html', 'text/rest', 'text/markdown') |
|
794 |
|
795 def render(self, form, renderer): |
|
796 wdgs = [super(EditableFileField, self).render(form, renderer)] |
|
797 if self.format(form) in self.editable_formats: |
|
798 data = self.typed_value(form, load_bytes=True) |
|
799 if data: |
|
800 encoding = self.encoding(form) |
|
801 try: |
|
802 form.formvalues[(self, form)] = data.getvalue().decode(encoding) |
|
803 except UnicodeError: |
|
804 pass |
|
805 else: |
|
806 if not self.required: |
|
807 msg = form._cw._( |
|
808 'You can either submit a new file using the browse button above' |
|
809 ', or choose to remove already uploaded file by checking the ' |
|
810 '"detach attached file" check-box, or edit file content online ' |
|
811 'with the widget below.') |
|
812 else: |
|
813 msg = form._cw._( |
|
814 'You can either submit a new file using the browse button above' |
|
815 ', or edit file content online with the widget below.') |
|
816 wdgs.append(u'<p><b>%s</b></p>' % msg) |
|
817 wdgs.append(fw.TextArea(setdomid=False).render(form, self, renderer)) |
|
818 # XXX restore form context? |
|
819 return '\n'.join(wdgs) |
|
820 |
|
821 def _process_form_value(self, form): |
|
822 value = form._cw.form.get(self.input_name(form)) |
|
823 if isinstance(value, text_type): |
|
824 # file modified using a text widget |
|
825 return Binary(value.encode(self.encoding(form))) |
|
826 return super(EditableFileField, self)._process_form_value(form) |
|
827 |
|
828 |
|
829 class BigIntField(Field): |
|
830 """Use this field to edit big integers (`BigInt` yams type). This field |
|
831 additionally support `min` and `max` attributes that specify a minimum and/or |
|
832 maximum value for the integer (`None` meaning no boundary). |
|
833 |
|
834 Unless explicitly specified, the widget for this field will be a |
|
835 :class:`~cubicweb.web.formwidgets.TextInput`. |
|
836 """ |
|
837 default_text_input_size = 10 |
|
838 |
|
839 def __init__(self, min=None, max=None, **kwargs): |
|
840 super(BigIntField, self).__init__(**kwargs) |
|
841 self.min = min |
|
842 self.max = max |
|
843 |
|
844 def init_widget(self, widget): |
|
845 super(BigIntField, self).init_widget(widget) |
|
846 if isinstance(self.widget, fw.TextInput): |
|
847 self.widget.attrs.setdefault('size', self.default_text_input_size) |
|
848 |
|
849 def _ensure_correctly_typed(self, form, value): |
|
850 if isinstance(value, string_types): |
|
851 value = value.strip() |
|
852 if not value: |
|
853 return None |
|
854 try: |
|
855 return int(value) |
|
856 except ValueError: |
|
857 raise ProcessFormError(form._cw._('an integer is expected')) |
|
858 return value |
|
859 |
|
860 |
|
861 class IntField(BigIntField): |
|
862 """Use this field to edit integers (`Int` yams type). Similar to |
|
863 :class:`~cubicweb.web.formfields.BigIntField` but set max length when text |
|
864 input widget is used (the default). |
|
865 """ |
|
866 default_text_input_size = 5 |
|
867 |
|
868 def init_widget(self, widget): |
|
869 super(IntField, self).init_widget(widget) |
|
870 if isinstance(self.widget, fw.TextInput): |
|
871 self.widget.attrs.setdefault('maxlength', 15) |
|
872 |
|
873 |
|
874 class BooleanField(Field): |
|
875 """Use this field to edit booleans (`Boolean` yams type). |
|
876 |
|
877 Unless explicitly specified, the widget for this field will be a |
|
878 :class:`~cubicweb.web.formwidgets.Radio` with yes/no values. You |
|
879 can change that values by specifing `choices`. |
|
880 """ |
|
881 widget = fw.Radio |
|
882 |
|
883 def __init__(self, allow_none=False, **kwargs): |
|
884 super(BooleanField, self).__init__(**kwargs) |
|
885 self.allow_none = allow_none |
|
886 |
|
887 def vocabulary(self, form): |
|
888 if self.choices: |
|
889 return super(BooleanField, self).vocabulary(form) |
|
890 if self.allow_none: |
|
891 return [(form._cw._('indifferent'), ''), |
|
892 (form._cw._('yes'), '1'), |
|
893 (form._cw._('no'), '0')] |
|
894 # XXX empty string for 'no' in that case for bw compat |
|
895 return [(form._cw._('yes'), '1'), (form._cw._('no'), '')] |
|
896 |
|
897 def format_single_value(self, req, value): |
|
898 """return value suitable for display""" |
|
899 if self.allow_none: |
|
900 if value is None: |
|
901 return u'' |
|
902 if value is False: |
|
903 return '0' |
|
904 return super(BooleanField, self).format_single_value(req, value) |
|
905 |
|
906 def _ensure_correctly_typed(self, form, value): |
|
907 if self.allow_none: |
|
908 if value: |
|
909 return bool(int(value)) |
|
910 return None |
|
911 return bool(value) |
|
912 |
|
913 |
|
914 class FloatField(IntField): |
|
915 """Use this field to edit floats (`Float` yams type). This field additionally |
|
916 support `min` and `max` attributes as the |
|
917 :class:`~cubicweb.web.formfields.IntField`. |
|
918 |
|
919 Unless explicitly specified, the widget for this field will be a |
|
920 :class:`~cubicweb.web.formwidgets.TextInput`. |
|
921 """ |
|
922 def format_single_value(self, req, value): |
|
923 formatstr = req.property_value('ui.float-format') |
|
924 if value is None: |
|
925 return u'' |
|
926 return formatstr % float(value) |
|
927 |
|
928 def render_example(self, req): |
|
929 return self.format_single_value(req, 1.234) |
|
930 |
|
931 def _ensure_correctly_typed(self, form, value): |
|
932 if isinstance(value, string_types): |
|
933 value = value.strip() |
|
934 if not value: |
|
935 return None |
|
936 try: |
|
937 return float(value) |
|
938 except ValueError: |
|
939 raise ProcessFormError(form._cw._('a float is expected')) |
|
940 return None |
|
941 |
|
942 |
|
943 class TimeIntervalField(StringField): |
|
944 """Use this field to edit time interval (`Interval` yams type). |
|
945 |
|
946 Unless explicitly specified, the widget for this field will be a |
|
947 :class:`~cubicweb.web.formwidgets.TextInput`. |
|
948 """ |
|
949 widget = fw.TextInput |
|
950 |
|
951 def format_single_value(self, req, value): |
|
952 if value: |
|
953 value = format_time(value.days * 24 * 3600 + value.seconds) |
|
954 return text_type(value) |
|
955 return u'' |
|
956 |
|
957 def example_format(self, req): |
|
958 """return a sample string describing what can be given as input for this |
|
959 field |
|
960 """ |
|
961 return u'20s, 10min, 24h, 4d' |
|
962 |
|
963 def _ensure_correctly_typed(self, form, value): |
|
964 if isinstance(value, string_types): |
|
965 value = value.strip() |
|
966 if not value: |
|
967 return None |
|
968 try: |
|
969 value = apply_units(value, TIME_UNITS) |
|
970 except ValueError: |
|
971 raise ProcessFormError(form._cw._('a number (in seconds) or 20s, 10min, 24h or 4d are expected')) |
|
972 return timedelta(0, value) |
|
973 |
|
974 |
|
975 class DateField(StringField): |
|
976 """Use this field to edit date (`Date` yams type). |
|
977 |
|
978 Unless explicitly specified, the widget for this field will be a |
|
979 :class:`~cubicweb.web.formwidgets.JQueryDatePicker`. |
|
980 """ |
|
981 widget = fw.JQueryDatePicker |
|
982 format_prop = 'ui.date-format' |
|
983 etype = 'Date' |
|
984 |
|
985 def format_single_value(self, req, value): |
|
986 if value: |
|
987 return ustrftime(value, req.property_value(self.format_prop)) |
|
988 return u'' |
|
989 |
|
990 def render_example(self, req): |
|
991 return self.format_single_value(req, datetime.now()) |
|
992 |
|
993 def _ensure_correctly_typed(self, form, value): |
|
994 if isinstance(value, string_types): |
|
995 value = value.strip() |
|
996 if not value: |
|
997 return None |
|
998 try: |
|
999 value = form._cw.parse_datetime(value, self.etype) |
|
1000 except ValueError as ex: |
|
1001 raise ProcessFormError(text_type(ex)) |
|
1002 return value |
|
1003 |
|
1004 |
|
1005 class DateTimeField(DateField): |
|
1006 """Use this field to edit datetime (`Datetime` yams type). |
|
1007 |
|
1008 Unless explicitly specified, the widget for this field will be a |
|
1009 :class:`~cubicweb.web.formwidgets.JQueryDateTimePicker`. |
|
1010 """ |
|
1011 widget = fw.JQueryDateTimePicker |
|
1012 format_prop = 'ui.datetime-format' |
|
1013 etype = 'Datetime' |
|
1014 |
|
1015 |
|
1016 class TimeField(DateField): |
|
1017 """Use this field to edit time (`Time` yams type). |
|
1018 |
|
1019 Unless explicitly specified, the widget for this field will be a |
|
1020 :class:`~cubicweb.web.formwidgets.JQueryTimePicker`. |
|
1021 """ |
|
1022 widget = fw.JQueryTimePicker |
|
1023 format_prop = 'ui.time-format' |
|
1024 etype = 'Time' |
|
1025 |
|
1026 |
|
1027 # XXX use cases where we don't actually want a better widget? |
|
1028 class CompoundField(Field): |
|
1029 """This field shouldn't be used directly, it's designed to hold inner |
|
1030 fields that should be conceptually groupped together. |
|
1031 """ |
|
1032 def __init__(self, fields, *args, **kwargs): |
|
1033 super(CompoundField, self).__init__(*args, **kwargs) |
|
1034 self.fields = fields |
|
1035 |
|
1036 def subfields(self, form): |
|
1037 return self.fields |
|
1038 |
|
1039 def actual_fields(self, form): |
|
1040 # don't add [self] to actual fields, compound field is usually kinda |
|
1041 # virtual, all interesting values are in subfield. Skipping it may avoid |
|
1042 # error when processed by the editcontroller : it may be marked as required |
|
1043 # while it has no value, hence generating a false error. |
|
1044 return list(self.fields) |
|
1045 |
|
1046 @property |
|
1047 def needs_multipart(self): |
|
1048 return any(f.needs_multipart for f in self.fields) |
|
1049 |
|
1050 |
|
1051 class RelationField(Field): |
|
1052 """Use this field to edit a relation of an entity. |
|
1053 |
|
1054 Unless explicitly specified, the widget for this field will be a |
|
1055 :class:`~cubicweb.web.formwidgets.Select`. |
|
1056 """ |
|
1057 |
|
1058 @staticmethod |
|
1059 def fromcardinality(card, **kwargs): |
|
1060 kwargs.setdefault('widget', fw.Select(multiple=card in '*+')) |
|
1061 return RelationField(**kwargs) |
|
1062 |
|
1063 def choices(self, form, limit=None): |
|
1064 """Take care, choices function for relation field instance should take |
|
1065 an extra 'limit' argument, with default to None. |
|
1066 |
|
1067 This argument is used by the 'unrelateddivs' view (see in autoform) and |
|
1068 when it's specified (eg not None), vocabulary returned should: |
|
1069 * not include already related entities |
|
1070 * have a max size of `limit` entities |
|
1071 """ |
|
1072 entity = form.edited_entity |
|
1073 # first see if its specified by __linkto form parameters |
|
1074 if limit is None: |
|
1075 linkedto = self.relvoc_linkedto(form) |
|
1076 if linkedto: |
|
1077 return linkedto |
|
1078 # it isn't, search more vocabulary |
|
1079 vocab = self.relvoc_init(form) |
|
1080 else: |
|
1081 vocab = [] |
|
1082 vocab += self.relvoc_unrelated(form, limit) |
|
1083 if self.sort: |
|
1084 vocab = vocab_sort(vocab) |
|
1085 return vocab |
|
1086 |
|
1087 def relvoc_linkedto(self, form): |
|
1088 linkedto = form.linked_to.get((self.name, self.role)) |
|
1089 if linkedto: |
|
1090 buildent = form._cw.entity_from_eid |
|
1091 return [(buildent(eid).view('combobox'), text_type(eid)) |
|
1092 for eid in linkedto] |
|
1093 return [] |
|
1094 |
|
1095 def relvoc_init(self, form): |
|
1096 entity, rtype, role = form.edited_entity, self.name, self.role |
|
1097 vocab = [] |
|
1098 if not self.required: |
|
1099 vocab.append(('', INTERNAL_FIELD_VALUE)) |
|
1100 # vocabulary doesn't include current values, add them |
|
1101 if form.edited_entity.has_eid(): |
|
1102 rset = form.edited_entity.related(self.name, self.role) |
|
1103 vocab += [(e.view('combobox'), text_type(e.eid)) |
|
1104 for e in rset.entities()] |
|
1105 return vocab |
|
1106 |
|
1107 def relvoc_unrelated(self, form, limit=None): |
|
1108 entity = form.edited_entity |
|
1109 rtype = entity._cw.vreg.schema.rschema(self.name) |
|
1110 if entity.has_eid(): |
|
1111 done = set(row[0] for row in entity.related(rtype, self.role)) |
|
1112 else: |
|
1113 done = None |
|
1114 result = [] |
|
1115 rsetsize = None |
|
1116 for objtype in rtype.targets(entity.e_schema, self.role): |
|
1117 if limit is not None: |
|
1118 rsetsize = limit - len(result) |
|
1119 result += self._relvoc_unrelated(form, objtype, rsetsize, done) |
|
1120 if limit is not None and len(result) >= limit: |
|
1121 break |
|
1122 return result |
|
1123 |
|
1124 def _relvoc_unrelated(self, form, targettype, limit, done): |
|
1125 """return unrelated entities for a given relation and target entity type |
|
1126 for use in vocabulary |
|
1127 """ |
|
1128 if done is None: |
|
1129 done = set() |
|
1130 res = [] |
|
1131 entity = form.edited_entity |
|
1132 for entity in entity.unrelated(self.name, targettype, self.role, limit, |
|
1133 lt_infos=form.linked_to).entities(): |
|
1134 if entity.eid in done: |
|
1135 continue |
|
1136 done.add(entity.eid) |
|
1137 res.append((entity.view('combobox'), text_type(entity.eid))) |
|
1138 return res |
|
1139 |
|
1140 def format_single_value(self, req, value): |
|
1141 return text_type(value) |
|
1142 |
|
1143 def process_form_value(self, form): |
|
1144 """process posted form and return correctly typed value""" |
|
1145 try: |
|
1146 return form.formvalues[(self, form)] |
|
1147 except KeyError: |
|
1148 value = self._process_form_value(form) |
|
1149 # if value is None, there are some remaining pending fields, we'll |
|
1150 # have to recompute this later -> don't cache in formvalues |
|
1151 if value is not None: |
|
1152 form.formvalues[(self, form)] = value |
|
1153 return value |
|
1154 |
|
1155 def _process_form_value(self, form): |
|
1156 """process posted form and return correctly typed value""" |
|
1157 widget = self.get_widget(form) |
|
1158 values = widget.process_field_data(form, self) |
|
1159 if values is None: |
|
1160 values = () |
|
1161 elif not isinstance(values, list): |
|
1162 values = (values,) |
|
1163 eids = set() |
|
1164 rschema = form._cw.vreg.schema.rschema(self.name) |
|
1165 for eid in values: |
|
1166 if not eid or eid == INTERNAL_FIELD_VALUE: |
|
1167 continue |
|
1168 typed_eid = form.actual_eid(eid) |
|
1169 # if entity doesn't exist yet |
|
1170 if typed_eid is None: |
|
1171 # inlined relations of to-be-created **subject entities** have |
|
1172 # to be handled separatly |
|
1173 if self.role == 'object' and rschema.inlined: |
|
1174 form._cw.data['pending_inlined'][eid].add( (form, self) ) |
|
1175 else: |
|
1176 form._cw.data['pending_others'].add( (form, self) ) |
|
1177 return None |
|
1178 eids.add(typed_eid) |
|
1179 return eids |
|
1180 |
|
1181 @staticmethod |
|
1182 def no_value(value): |
|
1183 """return True if the value can be considered as no value for the field""" |
|
1184 # value is None is the 'not yet ready value, consider the empty set |
|
1185 return value is not None and not value |
|
1186 |
|
1187 |
|
1188 _AFF_KWARGS = uicfg.autoform_field_kwargs |
|
1189 |
|
1190 def guess_field(eschema, rschema, role='subject', req=None, **kwargs): |
|
1191 """This function return the most adapted field to edit the given relation |
|
1192 (`rschema`) where the given entity type (`eschema`) is the subject or object |
|
1193 (`role`). |
|
1194 |
|
1195 The field is initialized according to information found in the schema, |
|
1196 though any value can be explicitly specified using `kwargs`. |
|
1197 """ |
|
1198 fieldclass = None |
|
1199 rdef = eschema.rdef(rschema, role) |
|
1200 if role == 'subject': |
|
1201 targetschema = rdef.object |
|
1202 if rschema.final: |
|
1203 if rdef.get('internationalizable'): |
|
1204 kwargs.setdefault('internationalizable', True) |
|
1205 else: |
|
1206 targetschema = rdef.subject |
|
1207 card = rdef.role_cardinality(role) |
|
1208 kwargs['name'] = rschema.type |
|
1209 kwargs['role'] = role |
|
1210 kwargs['eidparam'] = True |
|
1211 kwargs.setdefault('required', card in '1+') |
|
1212 if role == 'object': |
|
1213 kwargs.setdefault('label', (eschema.type, rschema.type + '_object')) |
|
1214 else: |
|
1215 kwargs.setdefault('label', (eschema.type, rschema.type)) |
|
1216 kwargs.setdefault('help', rdef.description) |
|
1217 if rschema.final: |
|
1218 fieldclass = FIELDS[targetschema] |
|
1219 if fieldclass is StringField: |
|
1220 if eschema.has_metadata(rschema, 'format'): |
|
1221 # use RichTextField instead of StringField if the attribute has |
|
1222 # a "format" metadata. But getting information from constraints |
|
1223 # may be useful anyway... |
|
1224 for cstr in rdef.constraints: |
|
1225 if isinstance(cstr, StaticVocabularyConstraint): |
|
1226 raise Exception('rich text field with static vocabulary') |
|
1227 return RichTextField(**kwargs) |
|
1228 # init StringField parameters according to constraints |
|
1229 for cstr in rdef.constraints: |
|
1230 if isinstance(cstr, StaticVocabularyConstraint): |
|
1231 kwargs.setdefault('choices', cstr.vocabulary) |
|
1232 break |
|
1233 for cstr in rdef.constraints: |
|
1234 if isinstance(cstr, SizeConstraint) and cstr.max is not None: |
|
1235 kwargs['max_length'] = cstr.max |
|
1236 return StringField(**kwargs) |
|
1237 if fieldclass is FileField: |
|
1238 if req: |
|
1239 aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req) |
|
1240 else: |
|
1241 aff_kwargs = _AFF_KWARGS |
|
1242 for metadata in KNOWN_METAATTRIBUTES: |
|
1243 metaschema = eschema.has_metadata(rschema, metadata) |
|
1244 if metaschema is not None: |
|
1245 metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject') |
|
1246 kwargs['%s_field' % metadata] = guess_field(eschema, metaschema, |
|
1247 req=req, **metakwargs) |
|
1248 return fieldclass(**kwargs) |
|
1249 return RelationField.fromcardinality(card, **kwargs) |
|
1250 |
|
1251 |
|
1252 FIELDS = { |
|
1253 'String' : StringField, |
|
1254 'Bytes': FileField, |
|
1255 'Password': PasswordField, |
|
1256 |
|
1257 'Boolean': BooleanField, |
|
1258 'Int': IntField, |
|
1259 'BigInt': BigIntField, |
|
1260 'Float': FloatField, |
|
1261 'Decimal': StringField, |
|
1262 |
|
1263 'Date': DateField, |
|
1264 'Datetime': DateTimeField, |
|
1265 'TZDatetime': DateTimeField, |
|
1266 'Time': TimeField, |
|
1267 'TZTime': TimeField, |
|
1268 'Interval': TimeIntervalField, |
|
1269 } |