web/views/forms.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 21 Dec 2009 20:00:18 +0100
changeset 4161 4273f5094651
parent 4159 6b2b20c73d59
child 4162 d2663bcf5306
permissions -rw-r--r--
refactor vocabulary handling to avoid having to define methods on form objects, use .choices instead even for non final relations. Details: * deprecated support of <role>_<relation>_vocabulary methods on forms * new utility functions for relations vocabulary * most logic now on the RelationField class

"""some base form classes for CubicWeb web client

:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

from warnings import warn

from logilab.common.compat import any
from logilab.common.deprecation import deprecated

from cubicweb import typed_eid
from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
from cubicweb.web import form, formwidgets as fwdgs
from cubicweb.web.controller import NAV_FORM_PARAMETERS
from cubicweb.web.formfields import StringField, relvoc_unrelated


class FieldsForm(form.Form):
    """base class for fields based forms.

    The following attributes may be either set on subclasses or given on
    form selection to customize the generated form:

    * `needs_js`: sequence of javascript files that should be added to handle
      this form (through `req.add_js`)

    * `needs_css`: sequence of css files that should be added to handle this
      form (through `req.add_css`)

    * `domid`: value for the "id" attribute of the <form> tag

    * `action`: value for the "action" attribute of the <form> tag

    * `onsubmit`: value for the "onsubmit" attribute of the <form> tag

    * `cssclass`: value for the "class" attribute of the <form> tag

    * `cssstyle`: value for the "style" attribute of the <form> tag

    * `cwtarget`: value for the "cubicweb:target" attribute of the <form> tag

    * `redirect_path`: relative to redirect to after submitting the form

    * `copy_nav_params`: flag telling if navigation paramenters should be copied
      back in hidden input

    * `form_buttons`:  form buttons sequence (button widgets instances)

    * `form_renderer_id`: id of the form renderer to use to render the form

    * `fieldsets_in_order`: fieldset name sequence, to control order
    """
    __regid__ = 'base'

    internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS

    # attributes overrideable by subclasses or through __init__
    needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
    needs_css = ('cubicweb.form.css',)
    domid = 'form'
    action = None
    onsubmit = "return freezeFormButtons('%(domid)s');"
    cssclass = None
    cssstyle = None
    cwtarget = None
    redirect_path = None
    copy_nav_params = False
    form_buttons = None
    form_renderer_id = 'default'
    fieldsets_in_order = None

    def __init__(self, req, rset=None, row=None, col=None,
                 submitmsg=None, mainform=True,
                 **kwargs):
        super(FieldsForm, self).__init__(req, rset=rset, row=row, col=col)
        self.fields = list(self.__class__._fields_)
        for key, val in kwargs.items():
            if key in NAV_FORM_PARAMETERS:
                self.form_add_hidden(key, val)
            elif hasattr(self.__class__, key) and not key[0] == '_':
                setattr(self, key, val)
            else:
                self.cw_extra_kwargs[key] = val
            # skip other parameters, usually given for selection
            # (else write a custom class to handle them)
        if mainform:
            self.form_add_hidden('__errorurl', self.session_key())
            self.form_add_hidden('__domid', self.domid)
            self.restore_previous_post(self.session_key())

        # XXX why do we need two different variables (mainform and copy_nav_params ?)
        if self.copy_nav_params:
            for param in NAV_FORM_PARAMETERS:
                if not param in kwargs:
                    value = req.form.get(param)
                    if value:
                        self.form_add_hidden(param, value)
        if submitmsg is not None:
            self.form_add_hidden('__message', submitmsg)
        self.context = None
        if 'domid' in kwargs:# session key changed
            self.restore_previous_post(self.session_key())

    @property
    def form_needs_multipart(self):
        """true if the form needs enctype=multipart/form-data"""
        return any(field.needs_multipart for field in self.fields)

    def form_add_hidden(self, name, value=None, **kwargs):
        """add an hidden field to the form"""
        kwargs.setdefault('widget', fwdgs.HiddenInput)
        field = StringField(name=name, value=value, **kwargs)
        if 'id' in kwargs:
            # by default, hidden input don't set id attribute. If one is
            # explicitly specified, ensure it will be set
            field.widget.setdomid = True
        self.append_field(field)
        return field

    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, rendervalues=None, renderer=None):
        """render this form, using the renderer given in args or the default
        FormRenderer()
        """
        self.build_context(formvalues or {})
        if renderer is None:
            renderer = self.form_default_renderer()
        return renderer.render(self, rendervalues or {})

    def form_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)

    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 build (eg
        # __linkto handling for instance)
        for field in self.fields[:]:
            for field in field.actual_fields(self):
                field.form_init(self)

    def form_field_error(self, field):
        """return validation error for widget's field, if any"""
        if self._field_has_error(field):
            self.form_displayed_errors.add(field.name)
            return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
        return u''

    def form_field_format(self, field):
        """return MIME type used for the given (text or bytes) field"""
        return self._cw.property_value('ui.default-text-format')

    def form_field_encoding(self, field):
        """return encoding used for the given (text) field"""
        return self._cw.encoding

    def form_field_modified(self, field):
        return field.is_visible()

    def _field_has_error(self, field):
        """return true if the field has some error in given validation exception
        """
        return self.form_valerror and field.name in self.form_valerror.errors

    @deprecated('use .render(formvalues, rendervalues)')
    def form_render(self, **values):
        """render this form, using the renderer given in args or the default
        FormRenderer()
        """
        self.build_context(values)
        renderer = values.pop('renderer', None)
        if renderer is None:
            renderer = self.form_default_renderer()
        return renderer.render(self, values)


class EntityFieldsForm(FieldsForm):
    __regid__ = 'base'
    __select__ = (match_kwargs('entity')
                  | (one_line_rset() & non_final_entity()))

    internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
    domid = 'entityForm'

    def __init__(self, *args, **kwargs):
        self.edited_entity = kwargs.pop('entity', None)
        msg = kwargs.pop('submitmsg', None)
        super(EntityFieldsForm, self).__init__(*args, **kwargs)
        if self.edited_entity is None:
            self.edited_entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
        self.form_add_hidden('__type', eidparam=True)
        self.form_add_hidden('eid')
        if kwargs.get('mainform', True): # mainform default to true in parent
            self.form_add_hidden(u'__maineid', self.edited_entity.eid)
            # If we need to directly attach the new object to another one
            if self._cw.list_form_param('__linkto'):
                for linkto in self._cw.list_form_param('__linkto'):
                    self.form_add_hidden('__linkto', linkto)
                if msg:
                    msg = '%s %s' % (msg, self._cw._('and linked'))
                else:
                    msg = self._cw._('entity linked')
        if msg:
            self.form_add_hidden('__message', msg)

    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
        """
        try:
            return self.force_session_key
        except AttributeError:
            # XXX if this is a json request, suppose we should redirect to the
            # entity primary view
            if self.req.json_request:
                return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
            return '%s#%s' % (self.req.url(), self.domid)

    def build_context(self, formvalues=None):
        super(EntityFieldsForm, self).build_context(formvalues)
        edited = set()
        for field in self.fields:
            if field.eidparam:
                edited.add(field.role_name())
        self.add_hidden('_cw_edited_fields', u','.join(edited),
                        eidparam=True)

    def _field_has_error(self, field):
        """return true if the field has some error in given validation exception
        """
        return super(EntityFieldsForm, self)._field_has_error(field) \
               and self.form_valerror.eid == self.edited_entity.eid

        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 form_field_format(self, field):
        """return MIME type used for the given (text or bytes) field"""
        entity = self.edited_entity
        if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
            entity.has_eid() or '%s_format' % field.name in entity):
            return self.edited_entity.attr_metadata(field.name, 'format')
        return self._cw.property_value('ui.default-text-format')

    def form_field_encoding(self, field):
        """return encoding used for the given (text) field"""
        entity = self.edited_entity
        if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
            entity.has_eid() or '%s_encoding' % field.name in entity):
            return self.edited_entity.attr_metadata(field.name, 'encoding')
        return super(EntityFieldsForm, self).form_field_encoding(field)
    # XXX all this vocabulary handling should be on the field, no?
    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 typed_eid(eid)
        except ValueError:
            try:

    def form_field_modified(self, field):
        if field.is_visible():
            # fields not corresponding to an entity attribute / relations
            # are considered modified
            if not field.eidparam or not self.edited_entity.has_eid():
                return True # XXX
            try:
                if field.role == 'subject':
                    previous_value = getattr(self.edited_entity, field.name)
                else:
                    previous_value = getattr(self.edited_entity,
                                             'reverse_%s' % field.name)
            except AttributeError:
                # fields with eidparam=True but not corresponding to an actual
                # attribute or relation
                return True
            # if it's a non final relation, we need the eids
            if isinstance(previous_value, list):
                # widget should return untyped eids
                previous_value = set(unicode(e.eid) for e in previous_value)
            new_value = field.process_form_value(self)
            if self.edited_entity.has_eid() and (previous_value == new_value):
                return False # not modified
            return True
        return False
                return self._cw.data['eidmap'][eid]
            except KeyError:
                self._cw.data['eidmap'][eid] = None
                return None

    def editable_relations(self):
        return ()

    def should_display_add_new_relation_link(self, rschema, existant, card):
        return False

    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
    def subject_relation_vocabulary(self, rtype, limit=None):
        """defaut vocabulary method for the given relation, looking for
        relation's object entities (i.e. self is the subject)
        """
        return relvoc_unrelated(self.edited_entity, rtype, 'subject', limit=None)

    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
    def object_relation_vocabulary(self, rtype, limit=None):
        return relvoc_unrelated(self.edited_entity, rtype, 'object', limit=None)


class CompositeFormMixIn(object):
    """form composed of sub-forms"""
    __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):
    pass

class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm):
    pass # XXX why is this class necessary?