web/views/forms.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
--- a/web/views/forms.py	Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,483 +0,0 @@
-# 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?