|
1 # copyright 2003-2014 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 Base form classes |
|
20 ----------------- |
|
21 |
|
22 .. Note: |
|
23 |
|
24 Form is the glue that bind a context to a set of fields, and is rendered |
|
25 using a form renderer. No display is actually done here, though you'll find |
|
26 some attributes of form that are used to control the rendering process. |
|
27 |
|
28 Besides the automagic form we'll see later, there are roughly two main |
|
29 form classes in |cubicweb|: |
|
30 |
|
31 .. autoclass:: cubicweb.web.views.forms.FieldsForm |
|
32 .. autoclass:: cubicweb.web.views.forms.EntityFieldsForm |
|
33 |
|
34 As you have probably guessed, choosing between them is easy. Simply ask you the |
|
35 question 'I am editing an entity or not?'. If the answer is yes, use |
|
36 :class:`EntityFieldsForm`, else use :class:`FieldsForm`. |
|
37 |
|
38 Actually there exists a third form class: |
|
39 |
|
40 .. autoclass:: cubicweb.web.views.forms.CompositeForm |
|
41 |
|
42 but you'll use this one rarely. |
|
43 """ |
|
44 |
|
45 __docformat__ = "restructuredtext en" |
|
46 |
|
47 |
|
48 import time |
|
49 import inspect |
|
50 |
|
51 from six import text_type |
|
52 |
|
53 from logilab.common import dictattr, tempattr |
|
54 from logilab.common.decorators import iclassmethod, cached |
|
55 from logilab.common.textutils import splitstrip |
|
56 |
|
57 from cubicweb import ValidationError, neg_role |
|
58 from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset |
|
59 from cubicweb.web import RequestError, ProcessFormError |
|
60 from cubicweb.web import form |
|
61 from cubicweb.web.views import uicfg |
|
62 from cubicweb.web.formfields import guess_field |
|
63 |
|
64 |
|
65 class FieldsForm(form.Form): |
|
66 """This is the base class for fields based forms. |
|
67 |
|
68 **Attributes** |
|
69 |
|
70 The following attributes may be either set on subclasses or given on |
|
71 form selection to customize the generated form: |
|
72 |
|
73 :attr:`needs_js` |
|
74 sequence of javascript files that should be added to handle this form |
|
75 (through :meth:`~cubicweb.web.request.Request.add_js`) |
|
76 |
|
77 :attr:`needs_css` |
|
78 sequence of css files that should be added to handle this form (through |
|
79 :meth:`~cubicweb.web.request.Request.add_css`) |
|
80 |
|
81 :attr:`domid` |
|
82 value for the "id" attribute of the <form> tag |
|
83 |
|
84 :attr:`action` |
|
85 value for the "action" attribute of the <form> tag |
|
86 |
|
87 :attr:`onsubmit` |
|
88 value for the "onsubmit" attribute of the <form> tag |
|
89 |
|
90 :attr:`cssclass` |
|
91 value for the "class" attribute of the <form> tag |
|
92 |
|
93 :attr:`cssstyle` |
|
94 value for the "style" attribute of the <form> tag |
|
95 |
|
96 :attr:`cwtarget` |
|
97 value for the "target" attribute of the <form> tag |
|
98 |
|
99 :attr:`redirect_path` |
|
100 relative to redirect to after submitting the form |
|
101 |
|
102 :attr:`copy_nav_params` |
|
103 flag telling if navigation parameters should be copied back in hidden |
|
104 inputs |
|
105 |
|
106 :attr:`form_buttons` |
|
107 sequence of form control (:class:`~cubicweb.web.formwidgets.Button` |
|
108 widgets instances) |
|
109 |
|
110 :attr:`form_renderer_id` |
|
111 identifier of the form renderer to use to render the form |
|
112 |
|
113 :attr:`fieldsets_in_order` |
|
114 sequence of fieldset names , to control order |
|
115 |
|
116 :attr:`autocomplete` |
|
117 set to False to add 'autocomplete=off' in the form open tag |
|
118 |
|
119 **Generic methods** |
|
120 |
|
121 .. automethod:: cubicweb.web.form.Form.field_by_name(name, role=None) |
|
122 .. automethod:: cubicweb.web.form.Form.fields_by_name(name, role=None) |
|
123 |
|
124 **Form construction methods** |
|
125 |
|
126 .. automethod:: cubicweb.web.form.Form.remove_field(field) |
|
127 .. automethod:: cubicweb.web.form.Form.append_field(field) |
|
128 .. automethod:: cubicweb.web.form.Form.insert_field_before(field, name, role=None) |
|
129 .. automethod:: cubicweb.web.form.Form.insert_field_after(field, name, role=None) |
|
130 .. automethod:: cubicweb.web.form.Form.add_hidden(name, value=None, **kwargs) |
|
131 |
|
132 **Form rendering methods** |
|
133 |
|
134 .. automethod:: cubicweb.web.views.forms.FieldsForm.render |
|
135 |
|
136 **Form posting methods** |
|
137 |
|
138 Once a form is posted, you can retrieve the form on the controller side and |
|
139 use the following methods to ease processing. For "simple" forms, this |
|
140 should looks like : |
|
141 |
|
142 .. sourcecode :: python |
|
143 |
|
144 form = self._cw.vreg['forms'].select('myformid', self._cw) |
|
145 posted = form.process_posted() |
|
146 # do something with the returned dictionary |
|
147 |
|
148 Notice that form related to entity edition should usually use the |
|
149 `edit` controller which will handle all the logic for you. |
|
150 |
|
151 .. automethod:: cubicweb.web.views.forms.FieldsForm.process_posted |
|
152 .. automethod:: cubicweb.web.views.forms.FieldsForm.iter_modified_fields |
|
153 """ |
|
154 __regid__ = 'base' |
|
155 |
|
156 |
|
157 # attributes overrideable by subclasses or through __init__ |
|
158 needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',) |
|
159 needs_css = ('cubicweb.form.css',) |
|
160 action = None |
|
161 cssclass = None |
|
162 cssstyle = None |
|
163 cwtarget = None |
|
164 redirect_path = None |
|
165 form_buttons = None |
|
166 form_renderer_id = 'default' |
|
167 fieldsets_in_order = None |
|
168 autocomplete = True |
|
169 |
|
170 @property |
|
171 def needs_multipart(self): |
|
172 """true if the form needs enctype=multipart/form-data""" |
|
173 return any(field.needs_multipart for field in self.fields) |
|
174 |
|
175 def _get_onsubmit(self): |
|
176 try: |
|
177 return self._onsubmit |
|
178 except AttributeError: |
|
179 return "return freezeFormButtons('%(domid)s');" % dictattr(self) |
|
180 def _set_onsubmit(self, value): |
|
181 self._onsubmit = value |
|
182 onsubmit = property(_get_onsubmit, _set_onsubmit) |
|
183 |
|
184 def add_media(self): |
|
185 """adds media (CSS & JS) required by this widget""" |
|
186 if self.needs_js: |
|
187 self._cw.add_js(self.needs_js) |
|
188 if self.needs_css: |
|
189 self._cw.add_css(self.needs_css) |
|
190 |
|
191 def render(self, formvalues=None, renderer=None, **kwargs): |
|
192 """Render this form, using the `renderer` given as argument or the |
|
193 default according to :attr:`form_renderer_id`. The rendered form is |
|
194 returned as a unicode string. |
|
195 |
|
196 `formvalues` is an optional dictionary containing values that will be |
|
197 considered as field's value. |
|
198 |
|
199 Extra keyword arguments will be given to renderer's :meth:`render` method. |
|
200 """ |
|
201 w = kwargs.pop('w', None) |
|
202 self.build_context(formvalues) |
|
203 if renderer is None: |
|
204 renderer = self.default_renderer() |
|
205 renderer.render(w, self, kwargs) |
|
206 |
|
207 def default_renderer(self): |
|
208 return self._cw.vreg['formrenderers'].select( |
|
209 self.form_renderer_id, self._cw, |
|
210 rset=self.cw_rset, row=self.cw_row, col=self.cw_col or 0) |
|
211 |
|
212 formvalues = None |
|
213 def build_context(self, formvalues=None): |
|
214 """build form context values (the .context attribute which is a |
|
215 dictionary with field instance as key associated to a dictionary |
|
216 containing field 'name' (qualified), 'id', 'value' (for display, always |
|
217 a string). |
|
218 """ |
|
219 if self.formvalues is not None: |
|
220 return # already built |
|
221 self.formvalues = formvalues or {} |
|
222 # use a copy in case fields are modified while context is built (eg |
|
223 # __linkto handling for instance) |
|
224 for field in self.fields[:]: |
|
225 for field in field.actual_fields(self): |
|
226 field.form_init(self) |
|
227 # store used field in an hidden input for later usage by a controller |
|
228 fields = set() |
|
229 eidfields = set() |
|
230 for field in self.fields: |
|
231 if field.eidparam: |
|
232 eidfields.add(field.role_name()) |
|
233 elif field.name not in self.control_fields: |
|
234 fields.add(field.role_name()) |
|
235 if fields: |
|
236 self.add_hidden('_cw_fields', u','.join(fields)) |
|
237 if eidfields: |
|
238 self.add_hidden('_cw_entity_fields', u','.join(eidfields), |
|
239 eidparam=True) |
|
240 |
|
241 _default_form_action_path = 'edit' |
|
242 def form_action(self): |
|
243 action = self.action |
|
244 if action is None: |
|
245 return self._cw.build_url(self._default_form_action_path) |
|
246 return action |
|
247 |
|
248 # controller form processing methods ####################################### |
|
249 |
|
250 def iter_modified_fields(self, editedfields=None, entity=None): |
|
251 """return a generator on field that has been modified by the posted |
|
252 form. |
|
253 """ |
|
254 if editedfields is None: |
|
255 try: |
|
256 editedfields = self._cw.form['_cw_fields'] |
|
257 except KeyError: |
|
258 raise RequestError(self._cw._('no edited fields specified')) |
|
259 entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4 # XXX |
|
260 for editedfield in splitstrip(editedfields): |
|
261 try: |
|
262 name, role = editedfield.split('-') |
|
263 except Exception: |
|
264 name = editedfield |
|
265 role = None |
|
266 if entityform: |
|
267 field = self.field_by_name(name, role, eschema=entity.e_schema) |
|
268 else: |
|
269 field = self.field_by_name(name, role) |
|
270 if field.has_been_modified(self): |
|
271 yield field |
|
272 |
|
273 def process_posted(self): |
|
274 """use this method to process the content posted by a simple form. it |
|
275 will return a dictionary with field names as key and typed value as |
|
276 associated value. |
|
277 """ |
|
278 with tempattr(self, 'formvalues', {}): # init fields value cache |
|
279 errors = [] |
|
280 processed = {} |
|
281 for field in self.iter_modified_fields(): |
|
282 try: |
|
283 for field, value in field.process_posted(self): |
|
284 processed[field.role_name()] = value |
|
285 except ProcessFormError as exc: |
|
286 errors.append((field, exc)) |
|
287 if errors: |
|
288 errors = dict((f.role_name(), text_type(ex)) for f, ex in errors) |
|
289 raise ValidationError(None, errors) |
|
290 return processed |
|
291 |
|
292 |
|
293 class EntityFieldsForm(FieldsForm): |
|
294 """This class is designed for forms used to edit some entities. It should |
|
295 handle for you all the underlying stuff necessary to properly work with the |
|
296 generic :class:`~cubicweb.web.views.editcontroller.EditController`. |
|
297 """ |
|
298 |
|
299 __regid__ = 'base' |
|
300 __select__ = (match_kwargs('entity') |
|
301 | (one_line_rset() & non_final_entity())) |
|
302 domid = 'entityForm' |
|
303 uicfg_aff = uicfg.autoform_field |
|
304 uicfg_affk = uicfg.autoform_field_kwargs |
|
305 |
|
306 @iclassmethod |
|
307 def field_by_name(cls_or_self, name, role=None, eschema=None): |
|
308 """return field with the given name and role. If field is not explicitly |
|
309 defined for the form but `eclass` is specified, guess_field will be |
|
310 called. |
|
311 """ |
|
312 try: |
|
313 return super(EntityFieldsForm, cls_or_self).field_by_name(name, role) |
|
314 except form.FieldNotFound: |
|
315 if eschema is None or role is None or not name in eschema.schema: |
|
316 raise |
|
317 rschema = eschema.schema.rschema(name) |
|
318 # XXX use a sample target type. Document this. |
|
319 tschemas = rschema.targets(eschema, role) |
|
320 fieldcls = cls_or_self.uicfg_aff.etype_get( |
|
321 eschema, rschema, role, tschemas[0]) |
|
322 kwargs = cls_or_self.uicfg_affk.etype_get( |
|
323 eschema, rschema, role, tschemas[0]) |
|
324 if kwargs is None: |
|
325 kwargs = {} |
|
326 if fieldcls: |
|
327 if not isinstance(fieldcls, type): |
|
328 return fieldcls # already and instance |
|
329 return fieldcls(name=name, role=role, eidparam=True, **kwargs) |
|
330 if isinstance(cls_or_self, type): |
|
331 req = None |
|
332 else: |
|
333 req = cls_or_self._cw |
|
334 field = guess_field(eschema, rschema, role, req=req, eidparam=True, **kwargs) |
|
335 if field is None: |
|
336 raise |
|
337 return field |
|
338 |
|
339 def __init__(self, _cw, rset=None, row=None, col=None, **kwargs): |
|
340 try: |
|
341 self.edited_entity = kwargs.pop('entity') |
|
342 except KeyError: |
|
343 self.edited_entity = rset.complete_entity(row or 0, col or 0) |
|
344 msg = kwargs.pop('submitmsg', None) |
|
345 super(EntityFieldsForm, self).__init__(_cw, rset, row, col, **kwargs) |
|
346 self.uicfg_aff = self._cw.vreg['uicfg'].select( |
|
347 'autoform_field', self._cw, entity=self.edited_entity) |
|
348 self.uicfg_affk = self._cw.vreg['uicfg'].select( |
|
349 'autoform_field_kwargs', self._cw, entity=self.edited_entity) |
|
350 self.add_hidden('__type', self.edited_entity.cw_etype, eidparam=True) |
|
351 |
|
352 self.add_hidden('eid', self.edited_entity.eid) |
|
353 self.add_generation_time() |
|
354 # mainform default to true in parent, hence default to True |
|
355 if kwargs.get('mainform', True) or kwargs.get('mainentity', False): |
|
356 self.add_hidden(u'__maineid', self.edited_entity.eid) |
|
357 # If we need to directly attach the new object to another one |
|
358 if '__linkto' in self._cw.form: |
|
359 if msg: |
|
360 msg = '%s %s' % (msg, self._cw._('and linked')) |
|
361 else: |
|
362 msg = self._cw._('entity linked') |
|
363 if msg: |
|
364 msgid = self._cw.set_redirect_message(msg) |
|
365 self.add_hidden('_cwmsgid', msgid) |
|
366 |
|
367 def add_generation_time(self): |
|
368 # use %f to prevent (unlikely) display in exponential format |
|
369 self.add_hidden('__form_generation_time', '%.6f' % time.time(), |
|
370 eidparam=True) |
|
371 |
|
372 def add_linkto_hidden(self): |
|
373 """add the __linkto hidden field used to directly attach the new object |
|
374 to an existing other one when the relation between those two is not |
|
375 already present in the form. |
|
376 |
|
377 Warning: this method must be called only when all form fields are setup |
|
378 """ |
|
379 for (rtype, role), eids in self.linked_to.items(): |
|
380 # if the relation is already setup by a form field, do not add it |
|
381 # in a __linkto hidden to avoid setting it twice in the controller |
|
382 try: |
|
383 self.field_by_name(rtype, role) |
|
384 except form.FieldNotFound: |
|
385 for eid in eids: |
|
386 self.add_hidden('__linkto', '%s:%s:%s' % (rtype, eid, role)) |
|
387 |
|
388 def render(self, *args, **kwargs): |
|
389 self.add_linkto_hidden() |
|
390 return super(EntityFieldsForm, self).render(*args, **kwargs) |
|
391 |
|
392 @property |
|
393 @cached |
|
394 def linked_to(self): |
|
395 linked_to = {} |
|
396 # case where this is an embeded creation form |
|
397 try: |
|
398 eid = int(self.cw_extra_kwargs['peid']) |
|
399 except (KeyError, ValueError): |
|
400 # When parent is being created, its eid is not numeric (e.g. 'A') |
|
401 # hence ValueError. |
|
402 pass |
|
403 else: |
|
404 ltrtype = self.cw_extra_kwargs['rtype'] |
|
405 ltrole = neg_role(self.cw_extra_kwargs['role']) |
|
406 linked_to[(ltrtype, ltrole)] = [eid] |
|
407 # now consider __linkto if the current form is the main form |
|
408 try: |
|
409 self.field_by_name('__maineid') |
|
410 except form.FieldNotFound: |
|
411 return linked_to |
|
412 for linkto in self._cw.list_form_param('__linkto'): |
|
413 ltrtype, eid, ltrole = linkto.split(':') |
|
414 linked_to.setdefault((ltrtype, ltrole), []).append(int(eid)) |
|
415 return linked_to |
|
416 |
|
417 def session_key(self): |
|
418 """return the key that may be used to store / retreive data about a |
|
419 previous post which failed because of a validation error |
|
420 """ |
|
421 if self.force_session_key is not None: |
|
422 return self.force_session_key |
|
423 # XXX if this is a json request, suppose we should redirect to the |
|
424 # entity primary view |
|
425 if self._cw.ajax_request and self.edited_entity.has_eid(): |
|
426 return '%s#%s' % (self.edited_entity.absolute_url(), self.domid) |
|
427 # XXX we should not consider some url parameters that may lead to |
|
428 # different url after a validation error |
|
429 return '%s#%s' % (self._cw.url(), self.domid) |
|
430 |
|
431 def default_renderer(self): |
|
432 return self._cw.vreg['formrenderers'].select( |
|
433 self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row, |
|
434 col=self.cw_col, entity=self.edited_entity) |
|
435 |
|
436 def should_display_add_new_relation_link(self, rschema, existant, card): |
|
437 return False |
|
438 |
|
439 # controller side method (eg POST reception handling) |
|
440 |
|
441 def actual_eid(self, eid): |
|
442 # should be either an int (existant entity) or a variable (to be |
|
443 # created entity) |
|
444 assert eid or eid == 0, repr(eid) # 0 is a valid eid |
|
445 try: |
|
446 return int(eid) |
|
447 except ValueError: |
|
448 try: |
|
449 return self._cw.data['eidmap'][eid] |
|
450 except KeyError: |
|
451 self._cw.data['eidmap'][eid] = None |
|
452 return None |
|
453 |
|
454 def editable_relations(self): |
|
455 return () |
|
456 |
|
457 |
|
458 class CompositeFormMixIn(object): |
|
459 __regid__ = 'composite' |
|
460 form_renderer_id = __regid__ |
|
461 |
|
462 def __init__(self, *args, **kwargs): |
|
463 super(CompositeFormMixIn, self).__init__(*args, **kwargs) |
|
464 self.forms = [] |
|
465 |
|
466 def add_subform(self, subform): |
|
467 """mark given form as a subform and append it""" |
|
468 subform.parent_form = self |
|
469 self.forms.append(subform) |
|
470 |
|
471 def build_context(self, formvalues=None): |
|
472 super(CompositeFormMixIn, self).build_context(formvalues) |
|
473 for form in self.forms: |
|
474 form.build_context(formvalues) |
|
475 |
|
476 |
|
477 class CompositeForm(CompositeFormMixIn, FieldsForm): |
|
478 """Form composed of sub-forms. Typical usage is edition of multiple entities |
|
479 at once. |
|
480 """ |
|
481 |
|
482 class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm): |
|
483 pass # XXX why is this class necessary? |