diff -r ea9eab290dcd -r e8032965f37a web/views/forms.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/forms.py Fri May 29 14:19:30 2009 +0200
@@ -0,0 +1,524 @@
+"""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.decorators import iclassmethod
+
+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 HiddenInitialValueField, StringField
+
+
+class FieldsForm(form.Form):
+ id = 'base'
+
+ 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 form.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'%s' % 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):
+ id = '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.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'))
+
+ 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_ 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, __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"""
+ id = 'composite'
+ form_renderer_id = id
+
+ 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)