--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/forms.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,483 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""
+Base form classes
+-----------------
+
+.. Note:
+
+ Form is the glue that bind a context to a set of fields, and is rendered
+ using a form renderer. No display is actually done here, though you'll find
+ some attributes of form that are used to control the rendering process.
+
+Besides the automagic form we'll see later, there are roughly two main
+form classes in |cubicweb|:
+
+.. autoclass:: cubicweb.web.views.forms.FieldsForm
+.. autoclass:: cubicweb.web.views.forms.EntityFieldsForm
+
+As you have probably guessed, choosing between them is easy. Simply ask you the
+question 'I am editing an entity or not?'. If the answer is yes, use
+:class:`EntityFieldsForm`, else use :class:`FieldsForm`.
+
+Actually there exists a third form class:
+
+.. autoclass:: cubicweb.web.views.forms.CompositeForm
+
+but you'll use this one rarely.
+"""
+
+__docformat__ = "restructuredtext en"
+
+
+import time
+import inspect
+
+from six import text_type
+
+from logilab.common import dictattr, tempattr
+from logilab.common.decorators import iclassmethod, cached
+from logilab.common.textutils import splitstrip
+
+from cubicweb import ValidationError, neg_role
+from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
+from cubicweb.web import RequestError, ProcessFormError
+from cubicweb.web import form
+from cubicweb.web.views import uicfg
+from cubicweb.web.formfields import guess_field
+
+
+class FieldsForm(form.Form):
+ """This is the base class for fields based forms.
+
+ **Attributes**
+
+ The following attributes may be either set on subclasses or given on
+ form selection to customize the generated form:
+
+ :attr:`needs_js`
+ sequence of javascript files that should be added to handle this form
+ (through :meth:`~cubicweb.web.request.Request.add_js`)
+
+ :attr:`needs_css`
+ sequence of css files that should be added to handle this form (through
+ :meth:`~cubicweb.web.request.Request.add_css`)
+
+ :attr:`domid`
+ value for the "id" attribute of the <form> tag
+
+ :attr:`action`
+ value for the "action" attribute of the <form> tag
+
+ :attr:`onsubmit`
+ value for the "onsubmit" attribute of the <form> tag
+
+ :attr:`cssclass`
+ value for the "class" attribute of the <form> tag
+
+ :attr:`cssstyle`
+ value for the "style" attribute of the <form> tag
+
+ :attr:`cwtarget`
+ value for the "target" attribute of the <form> tag
+
+ :attr:`redirect_path`
+ relative to redirect to after submitting the form
+
+ :attr:`copy_nav_params`
+ flag telling if navigation parameters should be copied back in hidden
+ inputs
+
+ :attr:`form_buttons`
+ sequence of form control (:class:`~cubicweb.web.formwidgets.Button`
+ widgets instances)
+
+ :attr:`form_renderer_id`
+ identifier of the form renderer to use to render the form
+
+ :attr:`fieldsets_in_order`
+ sequence of fieldset names , to control order
+
+ :attr:`autocomplete`
+ set to False to add 'autocomplete=off' in the form open tag
+
+ **Generic methods**
+
+ .. automethod:: cubicweb.web.form.Form.field_by_name(name, role=None)
+ .. automethod:: cubicweb.web.form.Form.fields_by_name(name, role=None)
+
+ **Form construction methods**
+
+ .. automethod:: cubicweb.web.form.Form.remove_field(field)
+ .. automethod:: cubicweb.web.form.Form.append_field(field)
+ .. automethod:: cubicweb.web.form.Form.insert_field_before(field, name, role=None)
+ .. automethod:: cubicweb.web.form.Form.insert_field_after(field, name, role=None)
+ .. automethod:: cubicweb.web.form.Form.add_hidden(name, value=None, **kwargs)
+
+ **Form rendering methods**
+
+ .. automethod:: cubicweb.web.views.forms.FieldsForm.render
+
+ **Form posting methods**
+
+ Once a form is posted, you can retrieve the form on the controller side and
+ use the following methods to ease processing. For "simple" forms, this
+ should looks like :
+
+ .. sourcecode :: python
+
+ form = self._cw.vreg['forms'].select('myformid', self._cw)
+ posted = form.process_posted()
+ # do something with the returned dictionary
+
+ Notice that form related to entity edition should usually use the
+ `edit` controller which will handle all the logic for you.
+
+ .. automethod:: cubicweb.web.views.forms.FieldsForm.process_posted
+ .. automethod:: cubicweb.web.views.forms.FieldsForm.iter_modified_fields
+ """
+ __regid__ = 'base'
+
+
+ # attributes overrideable by subclasses or through __init__
+ needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
+ needs_css = ('cubicweb.form.css',)
+ action = None
+ cssclass = None
+ cssstyle = None
+ cwtarget = None
+ redirect_path = None
+ form_buttons = None
+ form_renderer_id = 'default'
+ fieldsets_in_order = None
+ autocomplete = True
+
+ @property
+ def needs_multipart(self):
+ """true if the form needs enctype=multipart/form-data"""
+ return any(field.needs_multipart for field in self.fields)
+
+ def _get_onsubmit(self):
+ try:
+ return self._onsubmit
+ except AttributeError:
+ return "return freezeFormButtons('%(domid)s');" % dictattr(self)
+ def _set_onsubmit(self, value):
+ self._onsubmit = value
+ onsubmit = property(_get_onsubmit, _set_onsubmit)
+
+ def add_media(self):
+ """adds media (CSS & JS) required by this widget"""
+ if self.needs_js:
+ self._cw.add_js(self.needs_js)
+ if self.needs_css:
+ self._cw.add_css(self.needs_css)
+
+ def render(self, formvalues=None, renderer=None, **kwargs):
+ """Render this form, using the `renderer` given as argument or the
+ default according to :attr:`form_renderer_id`. The rendered form is
+ returned as a unicode string.
+
+ `formvalues` is an optional dictionary containing values that will be
+ considered as field's value.
+
+ Extra keyword arguments will be given to renderer's :meth:`render` method.
+ """
+ w = kwargs.pop('w', None)
+ self.build_context(formvalues)
+ if renderer is None:
+ renderer = self.default_renderer()
+ renderer.render(w, self, kwargs)
+
+ def default_renderer(self):
+ return self._cw.vreg['formrenderers'].select(
+ self.form_renderer_id, self._cw,
+ rset=self.cw_rset, row=self.cw_row, col=self.cw_col or 0)
+
+ formvalues = None
+ def build_context(self, formvalues=None):
+ """build form context values (the .context attribute which is a
+ dictionary with field instance as key associated to a dictionary
+ containing field 'name' (qualified), 'id', 'value' (for display, always
+ a string).
+ """
+ if self.formvalues is not None:
+ return # already built
+ self.formvalues = formvalues or {}
+ # use a copy in case fields are modified while context is built (eg
+ # __linkto handling for instance)
+ for field in self.fields[:]:
+ for field in field.actual_fields(self):
+ field.form_init(self)
+ # store used field in an hidden input for later usage by a controller
+ fields = set()
+ eidfields = set()
+ for field in self.fields:
+ if field.eidparam:
+ eidfields.add(field.role_name())
+ elif field.name not in self.control_fields:
+ fields.add(field.role_name())
+ if fields:
+ self.add_hidden('_cw_fields', u','.join(fields))
+ if eidfields:
+ self.add_hidden('_cw_entity_fields', u','.join(eidfields),
+ eidparam=True)
+
+ _default_form_action_path = 'edit'
+ def form_action(self):
+ action = self.action
+ if action is None:
+ return self._cw.build_url(self._default_form_action_path)
+ return action
+
+ # controller form processing methods #######################################
+
+ def iter_modified_fields(self, editedfields=None, entity=None):
+ """return a generator on field that has been modified by the posted
+ form.
+ """
+ if editedfields is None:
+ try:
+ editedfields = self._cw.form['_cw_fields']
+ except KeyError:
+ raise RequestError(self._cw._('no edited fields specified'))
+ entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4 # XXX
+ for editedfield in splitstrip(editedfields):
+ try:
+ name, role = editedfield.split('-')
+ except Exception:
+ name = editedfield
+ role = None
+ if entityform:
+ field = self.field_by_name(name, role, eschema=entity.e_schema)
+ else:
+ field = self.field_by_name(name, role)
+ if field.has_been_modified(self):
+ yield field
+
+ def process_posted(self):
+ """use this method to process the content posted by a simple form. it
+ will return a dictionary with field names as key and typed value as
+ associated value.
+ """
+ with tempattr(self, 'formvalues', {}): # init fields value cache
+ errors = []
+ processed = {}
+ for field in self.iter_modified_fields():
+ try:
+ for field, value in field.process_posted(self):
+ processed[field.role_name()] = value
+ except ProcessFormError as exc:
+ errors.append((field, exc))
+ if errors:
+ errors = dict((f.role_name(), text_type(ex)) for f, ex in errors)
+ raise ValidationError(None, errors)
+ return processed
+
+
+class EntityFieldsForm(FieldsForm):
+ """This class is designed for forms used to edit some entities. It should
+ handle for you all the underlying stuff necessary to properly work with the
+ generic :class:`~cubicweb.web.views.editcontroller.EditController`.
+ """
+
+ __regid__ = 'base'
+ __select__ = (match_kwargs('entity')
+ | (one_line_rset() & non_final_entity()))
+ domid = 'entityForm'
+ uicfg_aff = uicfg.autoform_field
+ uicfg_affk = uicfg.autoform_field_kwargs
+
+ @iclassmethod
+ def field_by_name(cls_or_self, name, role=None, eschema=None):
+ """return field with the given name and role. If field is not explicitly
+ defined for the form but `eclass` is specified, guess_field will be
+ called.
+ """
+ try:
+ return super(EntityFieldsForm, cls_or_self).field_by_name(name, role)
+ except form.FieldNotFound:
+ if eschema is None or role is None or not name in eschema.schema:
+ raise
+ rschema = eschema.schema.rschema(name)
+ # XXX use a sample target type. Document this.
+ tschemas = rschema.targets(eschema, role)
+ fieldcls = cls_or_self.uicfg_aff.etype_get(
+ eschema, rschema, role, tschemas[0])
+ kwargs = cls_or_self.uicfg_affk.etype_get(
+ eschema, rschema, role, tschemas[0])
+ if kwargs is None:
+ kwargs = {}
+ if fieldcls:
+ if not isinstance(fieldcls, type):
+ return fieldcls # already and instance
+ return fieldcls(name=name, role=role, eidparam=True, **kwargs)
+ if isinstance(cls_or_self, type):
+ req = None
+ else:
+ req = cls_or_self._cw
+ field = guess_field(eschema, rschema, role, req=req, eidparam=True, **kwargs)
+ if field is None:
+ raise
+ return field
+
+ def __init__(self, _cw, rset=None, row=None, col=None, **kwargs):
+ try:
+ self.edited_entity = kwargs.pop('entity')
+ except KeyError:
+ self.edited_entity = rset.complete_entity(row or 0, col or 0)
+ msg = kwargs.pop('submitmsg', None)
+ super(EntityFieldsForm, self).__init__(_cw, rset, row, col, **kwargs)
+ self.uicfg_aff = self._cw.vreg['uicfg'].select(
+ 'autoform_field', self._cw, entity=self.edited_entity)
+ self.uicfg_affk = self._cw.vreg['uicfg'].select(
+ 'autoform_field_kwargs', self._cw, entity=self.edited_entity)
+ self.add_hidden('__type', self.edited_entity.cw_etype, eidparam=True)
+
+ self.add_hidden('eid', self.edited_entity.eid)
+ self.add_generation_time()
+ # mainform default to true in parent, hence default to True
+ if kwargs.get('mainform', True) or kwargs.get('mainentity', False):
+ self.add_hidden(u'__maineid', self.edited_entity.eid)
+ # If we need to directly attach the new object to another one
+ if '__linkto' in self._cw.form:
+ if msg:
+ msg = '%s %s' % (msg, self._cw._('and linked'))
+ else:
+ msg = self._cw._('entity linked')
+ if msg:
+ msgid = self._cw.set_redirect_message(msg)
+ self.add_hidden('_cwmsgid', msgid)
+
+ def add_generation_time(self):
+ # use %f to prevent (unlikely) display in exponential format
+ self.add_hidden('__form_generation_time', '%.6f' % time.time(),
+ eidparam=True)
+
+ def add_linkto_hidden(self):
+ """add the __linkto hidden field used to directly attach the new object
+ to an existing other one when the relation between those two is not
+ already present in the form.
+
+ Warning: this method must be called only when all form fields are setup
+ """
+ for (rtype, role), eids in self.linked_to.items():
+ # if the relation is already setup by a form field, do not add it
+ # in a __linkto hidden to avoid setting it twice in the controller
+ try:
+ self.field_by_name(rtype, role)
+ except form.FieldNotFound:
+ for eid in eids:
+ self.add_hidden('__linkto', '%s:%s:%s' % (rtype, eid, role))
+
+ def render(self, *args, **kwargs):
+ self.add_linkto_hidden()
+ return super(EntityFieldsForm, self).render(*args, **kwargs)
+
+ @property
+ @cached
+ def linked_to(self):
+ linked_to = {}
+ # case where this is an embeded creation form
+ try:
+ eid = int(self.cw_extra_kwargs['peid'])
+ except (KeyError, ValueError):
+ # When parent is being created, its eid is not numeric (e.g. 'A')
+ # hence ValueError.
+ pass
+ else:
+ ltrtype = self.cw_extra_kwargs['rtype']
+ ltrole = neg_role(self.cw_extra_kwargs['role'])
+ linked_to[(ltrtype, ltrole)] = [eid]
+ # now consider __linkto if the current form is the main form
+ try:
+ self.field_by_name('__maineid')
+ except form.FieldNotFound:
+ return linked_to
+ for linkto in self._cw.list_form_param('__linkto'):
+ ltrtype, eid, ltrole = linkto.split(':')
+ linked_to.setdefault((ltrtype, ltrole), []).append(int(eid))
+ return linked_to
+
+ def session_key(self):
+ """return the key that may be used to store / retreive data about a
+ previous post which failed because of a validation error
+ """
+ if self.force_session_key is not None:
+ return self.force_session_key
+ # XXX if this is a json request, suppose we should redirect to the
+ # entity primary view
+ if self._cw.ajax_request and self.edited_entity.has_eid():
+ return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
+ # XXX we should not consider some url parameters that may lead to
+ # different url after a validation error
+ return '%s#%s' % (self._cw.url(), self.domid)
+
+ def default_renderer(self):
+ return self._cw.vreg['formrenderers'].select(
+ self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row,
+ col=self.cw_col, entity=self.edited_entity)
+
+ def should_display_add_new_relation_link(self, rschema, existant, card):
+ return False
+
+ # controller side method (eg POST reception handling)
+
+ def actual_eid(self, eid):
+ # should be either an int (existant entity) or a variable (to be
+ # created entity)
+ assert eid or eid == 0, repr(eid) # 0 is a valid eid
+ try:
+ return int(eid)
+ except ValueError:
+ try:
+ return self._cw.data['eidmap'][eid]
+ except KeyError:
+ self._cw.data['eidmap'][eid] = None
+ return None
+
+ def editable_relations(self):
+ return ()
+
+
+class CompositeFormMixIn(object):
+ __regid__ = 'composite'
+ form_renderer_id = __regid__
+
+ def __init__(self, *args, **kwargs):
+ super(CompositeFormMixIn, self).__init__(*args, **kwargs)
+ self.forms = []
+
+ def add_subform(self, subform):
+ """mark given form as a subform and append it"""
+ subform.parent_form = self
+ self.forms.append(subform)
+
+ def build_context(self, formvalues=None):
+ super(CompositeFormMixIn, self).build_context(formvalues)
+ for form in self.forms:
+ form.build_context(formvalues)
+
+
+class CompositeForm(CompositeFormMixIn, FieldsForm):
+ """Form composed of sub-forms. Typical usage is edition of multiple entities
+ at once.
+ """
+
+class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm):
+ pass # XXX why is this class necessary?