web/form.py
changeset 2005 e8032965f37a
parent 1995 ec95eaa2b711
child 2656 a93ae0f6c0ad
--- a/web/form.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/form.py	Fri May 29 14:19:30 2009 +0200
@@ -7,20 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-from warnings import warn
-
-from logilab.common.compat import any
-from logilab.common.decorators import iclassmethod
-
 from cubicweb.appobject import AppRsetObject
-from cubicweb.selectors import yes, non_final_entity, match_kwargs, one_line_rset
 from cubicweb.view import NOINDEX, NOFOLLOW
 from cubicweb.common import tags
-from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
-from cubicweb.web import formwidgets as fwdgs, httpcache
-from cubicweb.web.controller import NAV_FORM_PARAMETERS
-from cubicweb.web.formfields import (Field, StringField, RelationField,
-                                     HiddenInitialValueField)
+from cubicweb.web import stdmsgs, httpcache, formfields
 
 
 class FormViewMixIn(object):
@@ -198,7 +188,7 @@
             if hasattr(base, '_fields_'):
                 allfields += base._fields_
         clsfields = (item for item in classdict.items()
-                     if isinstance(item[1], Field))
+                     if isinstance(item[1], formfields.Field))
         for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
             if not field.name:
                 field.set_name(fieldname)
@@ -212,509 +202,6 @@
     found
     """
 
-class FieldsForm(FormMixIn, AppRsetObject):
+class Form(FormMixIn, AppRsetObject):
     __metaclass__ = metafieldsform
     __registry__ = 'forms'
-    __select__ = yes()
-
-    is_subform = False
-
-    # attributes overrideable through __init__
-    internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
-    needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
-    needs_css = ('cubicweb.form.css',)
-    domid = 'form'
-    title = None
-    action = None
-    onsubmit = "return freezeFormButtons('%(domid)s');"
-    cssclass = None
-    cssstyle = None
-    cwtarget = None
-    redirect_path = None
-    set_error_url = True
-    copy_nav_params = False
-    form_buttons = None # form buttons (button widgets instances)
-    form_renderer_id = 'default'
-
-    def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
-                 **kwargs):
-        super(FieldsForm, self).__init__(req, 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)
-            else:
-                assert hasattr(self.__class__, key) and not key[0] == '_', key
-                setattr(self, key, val)
-        if self.set_error_url:
-            self.form_add_hidden('__errorurl', self.session_key())
-        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())
-
-    @iclassmethod
-    def _fieldsattr(cls_or_self):
-        if isinstance(cls_or_self, type):
-            fields = cls_or_self._fields_
-        else:
-            fields = cls_or_self.fields
-        return fields
-
-    @iclassmethod
-    def field_by_name(cls_or_self, name, role='subject'):
-        """return field with the given name and role.
-        Raise FieldNotFound if the field can't be found.
-        """
-        for field in cls_or_self._fieldsattr():
-            if field.name == name and field.role == role:
-                return field
-        raise FieldNotFound(name)
-
-    @iclassmethod
-    def fields_by_name(cls_or_self, name, role='subject'):
-        """return a list of fields with the given name and role"""
-        return [field for field in cls_or_self._fieldsattr()
-                if field.name == name and field.role == role]
-
-    @iclassmethod
-    def remove_field(cls_or_self, field):
-        """remove a field from form class or instance"""
-        cls_or_self._fieldsattr().remove(field)
-
-    @iclassmethod
-    def append_field(cls_or_self, field):
-        """append a field to form class or instance"""
-        cls_or_self._fieldsattr().append(field)
-
-    @iclassmethod
-    def insert_field_before(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
-        fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field), new_field)
-
-    @iclassmethod
-    def insert_field_after(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
-        fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field)+1, new_field)
-
-    @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"""
-        field = StringField(name=name, widget=fwdgs.HiddenInput, initial=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.req.add_js(self.needs_js)
-        if self.needs_css:
-            self.req.add_css(self.needs_css)
-
-    def form_render(self, **values):
-        """render this form, using the renderer given in args or the default
-        FormRenderer()
-        """
-        renderer = values.pop('renderer', None)
-        if renderer is None:
-            renderer = self.form_default_renderer()
-        return renderer.render(self, values)
-
-    def form_default_renderer(self):
-        return self.vreg.select_object('formrenderers', self.form_renderer_id,
-                                       self.req, self.rset,
-                                       row=self.row, col=self.col)
-
-    def form_build_context(self, rendervalues=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).
-
-        rendervalues is an optional dictionary containing extra kwargs given to
-        form_render()
-        """
-        self.context = context = {}
-        # ensure rendervalues is a dict
-        if rendervalues is None:
-            rendervalues = {}
-        # 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)
-                value = self.form_field_display_value(field, rendervalues)
-                context[field] = {'value': value,
-                                  'name': self.form_field_name(field),
-                                  'id': self.form_field_id(field),
-                                  }
-
-    def form_field_display_value(self, field, rendervalues, load_bytes=False):
-        """return field's *string* value to use for display
-
-        looks in
-        1. previously submitted form values if any (eg on validation error)
-        2. req.form
-        3. extra kw args given to render_form
-        4. field's typed value
-
-        values found in 1. and 2. are expected te be already some 'display'
-        value while those found in 3. and 4. are expected to be correctly typed.
-        """
-        value = self._req_display_value(field)
-        if value is None:
-            if field.name in rendervalues:
-                value = rendervalues[field.name]
-            else:
-                value = self.form_field_value(field, load_bytes)
-                if callable(value):
-                    value = value(self)
-            if value != INTERNAL_FIELD_VALUE:
-                value = field.format_value(self.req, value)
-        return value
-
-    def _req_display_value(self, field):
-        qname = self.form_field_name(field)
-        if qname in self.form_previous_values:
-            return self.form_previous_values[qname]
-        if qname in self.req.form:
-            return self.req.form[qname]
-        if field.name in self.req.form:
-            return self.req.form[field.name]
-        return None
-
-    def form_field_value(self, field, load_bytes=False):
-        """return field's *typed* value"""
-        myattr = '%s_%s_default' % (field.role, field.name)
-        if hasattr(self, myattr):
-            return getattr(self, myattr)()
-        value = field.initial
-        if callable(value):
-            value = value(self)
-        return value
-
-    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.req.property_value('ui.default-text-format')
-
-    def form_field_encoding(self, field):
-        """return encoding used for the given (text) field"""
-        return self.req.encoding
-
-    def form_field_name(self, field):
-        """return qualified name for the given field"""
-        return field.name
-
-    def form_field_id(self, field):
-        """return dom id for the given field"""
-        return field.id
-
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field. Should be overriden in
-        specific forms using fields which requires some vocabulary
-        """
-        raise NotImplementedError
-
-    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
-
-
-class EntityFieldsForm(FieldsForm):
-    __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.complete_entity(self.row or 0, self.col or 0)
-        self.form_add_hidden('__type', eidparam=True)
-        self.form_add_hidden('eid')
-        if msg:
-            # If we need to directly attach the new object to another one
-            self.form_add_hidden('__message', msg)
-        if not self.is_subform:
-            for linkto in self.req.list_form_param('__linkto'):
-                self.form_add_hidden('__linkto', linkto)
-                msg = '%s %s' % (msg, self.req._('and linked'))
-        # in case of direct instanciation
-        self.schema = self.edited_entity.schema
-        self.vreg = self.edited_entity.vreg
-
-    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
-
-    def _relation_vocabulary(self, rtype, targettype, role,
-                            limit=None, done=None):
-        """return unrelated entities for a given relation and target entity type
-        for use in vocabulary
-        """
-        if done is None:
-            done = set()
-        rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
-        res = []
-        for entity in rset.entities():
-            if entity.eid in done:
-                continue
-            done.add(entity.eid)
-            res.append((entity.view('combobox'), entity.eid))
-        return res
-
-    def _req_display_value(self, field):
-        value = super(EntityFieldsForm, self)._req_display_value(field)
-        if value is None:
-            value = self.edited_entity.linked_to(field.name, field.role)
-            if value:
-                searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
-                                  for eid in value]
-                # remove associated __linkto hidden fields
-                for field in self.fields_by_name('__linkto'):
-                    if field.initial in searchedvalues:
-                        self.remove_field(field)
-            else:
-                value = None
-        return value
-
-    def _form_field_default_value(self, field, load_bytes):
-        defaultattr = 'default_%s' % field.name
-        if hasattr(self.edited_entity, defaultattr):
-            # XXX bw compat, default_<field name> on the entity
-            warn('found %s on %s, should be set on a specific form'
-                 % (defaultattr, self.edited_entity.id), DeprecationWarning)
-            value = getattr(self.edited_entity, defaultattr)
-            if callable(value):
-                value = value()
-        else:
-            value = super(EntityFieldsForm, self).form_field_value(field,
-                                                                   load_bytes)
-        return value
-
-    def form_default_renderer(self):
-        return self.vreg.select_object('formrenderers', self.form_renderer_id,
-                                       self.req, self.rset,
-                                       row=self.row, col=self.col,
-                                       entity=self.edited_entity)
-
-    def form_build_context(self, values=None):
-        """overriden to add edit[s|o] hidden fields and to ensure schema fields
-        have eidparam set to True
-
-        edit[s|o] hidden fields are used to indicate the value for the
-        associated field before the (potential) modification made when
-        submitting the form.
-        """
-        eschema = self.edited_entity.e_schema
-        for field in self.fields[:]:
-            for field in field.actual_fields(self):
-                fieldname = field.name
-                if fieldname != 'eid' and (
-                    (eschema.has_subject_relation(fieldname) or
-                     eschema.has_object_relation(fieldname))):
-                    field.eidparam = True
-                    self.fields.append(HiddenInitialValueField(field))
-        return super(EntityFieldsForm, self).form_build_context(values)
-
-    def form_field_value(self, field, load_bytes=False):
-        """return field's *typed* value
-
-        overriden to deal with
-        * special eid / __type / edits- / edito- fields
-        * lookup for values on edited entities
-        """
-        attr = field.name
-        entity = self.edited_entity
-        if attr == 'eid':
-            return entity.eid
-        if not field.eidparam:
-            return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
-        if attr.startswith('edits-') or attr.startswith('edito-'):
-            # edit[s|o]- fieds must have the actual value stored on the entity
-            assert hasattr(field, 'visible_field')
-            vfield = field.visible_field
-            assert vfield.eidparam
-            if entity.has_eid():
-                return self.form_field_value(vfield)
-            return INTERNAL_FIELD_VALUE
-        if attr == '__type':
-            return entity.id
-        if self.schema.rschema(attr).is_final():
-            attrtype = entity.e_schema.destination(attr)
-            if attrtype == 'Password':
-                return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
-            if attrtype == 'Bytes':
-                if entity.has_eid():
-                    if load_bytes:
-                        return getattr(entity, attr)
-                    # XXX value should reflect if some file is already attached
-                    return True
-                return False
-            if entity.has_eid() or attr in entity:
-                value = getattr(entity, attr)
-            else:
-                value = self._form_field_default_value(field, load_bytes)
-            return value
-        # non final relation field
-        if entity.has_eid() or entity.relation_cached(attr, field.role):
-            value = [r[0] for r in entity.related(attr, field.role)]
-        else:
-            value = self._form_field_default_value(field, load_bytes)
-        return value
-
-    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.req.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)
-
-    def form_field_name(self, field):
-        """return qualified name for the given field"""
-        if field.eidparam:
-            return eid_param(field.name, self.edited_entity.eid)
-        return field.name
-
-    def form_field_id(self, field):
-        """return dom id for the given field"""
-        if field.eidparam:
-            return eid_param(field.id, self.edited_entity.eid)
-        return field.id
-
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field"""
-        role, rtype = field.role, field.name
-        method = '%s_%s_vocabulary' % (role, rtype)
-        try:
-            vocabfunc = getattr(self, method)
-        except AttributeError:
-            try:
-                # XXX bw compat, <role>_<rtype>_vocabulary on the entity
-                vocabfunc = getattr(self.edited_entity, method)
-            except AttributeError:
-                vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
-            else:
-                warn('found %s on %s, should be set on a specific form'
-                     % (method, self.edited_entity.id), DeprecationWarning)
-        # NOTE: it is the responsibility of `vocabfunc` to sort the result
-        #       (direclty through RQL or via a python sort). This is also
-        #       important because `vocabfunc` might return a list with
-        #       couples (label, None) which act as separators. In these
-        #       cases, it doesn't make sense to sort results afterwards.
-        return vocabfunc(rtype, limit)
-
-    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)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = entity.schema.rschema(rtype)
-        done = None
-        assert not rtype.is_final(), rtype
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, str(rtype)))
-        result = []
-        rsetsize = None
-        for objtype in rtype.objects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, objtype, 'subject',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def object_relation_vocabulary(self, rtype, limit=None):
-        """defaut vocabulary method for the given relation, looking for
-        relation's subject entities (i.e. self is the object)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = entity.schema.rschema(rtype)
-        done = None
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
-        result = []
-        rsetsize = None
-        for subjtype in rtype.subjects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, subjtype, 'object',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def subject_in_state_vocabulary(self, rtype, limit=None):
-        """vocabulary method for the in_state relation, looking for relation's
-        object entities (i.e. self is the subject) according to initial_state,
-        state_of and next_state relation
-        """
-        entity = self.edited_entity
-        if not entity.has_eid() or not entity.in_state:
-            # get the initial state
-            rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
-            rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
-            if rset:
-                return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
-            return []
-        results = []
-        for tr in entity.in_state[0].transitions(entity):
-            state = tr.destination_state[0]
-            results.append((state.view('combobox'), state.eid))
-        return sorted(results)
-
-
-class CompositeForm(FieldsForm):
-    """form composed for sub-forms"""
-    form_renderer_id = 'composite'
-
-    def __init__(self, *args, **kwargs):
-        super(CompositeForm, self).__init__(*args, **kwargs)
-        self.forms = []
-
-    def form_add_subform(self, subform):
-        """mark given form as a subform and append it"""
-        subform.is_subform = True
-        self.forms.append(subform)