web/views/forms.py
changeset 2005 e8032965f37a
child 2045 bf0643d4ef36
equal deleted inserted replaced
2004:ea9eab290dcd 2005:e8032965f37a
       
     1 """some base form classes for CubicWeb web client
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from warnings import warn
       
    11 
       
    12 from logilab.common.compat import any
       
    13 from logilab.common.decorators import iclassmethod
       
    14 
       
    15 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
       
    16 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
       
    17 from cubicweb.web import form, formwidgets as fwdgs
       
    18 from cubicweb.web.controller import NAV_FORM_PARAMETERS
       
    19 from cubicweb.web.formfields import HiddenInitialValueField, StringField
       
    20 
       
    21 
       
    22 class FieldsForm(form.Form):
       
    23     id = 'base'
       
    24 
       
    25     is_subform = False
       
    26 
       
    27     # attributes overrideable through __init__
       
    28     internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
       
    29     needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
       
    30     needs_css = ('cubicweb.form.css',)
       
    31     domid = 'form'
       
    32     title = None
       
    33     action = None
       
    34     onsubmit = "return freezeFormButtons('%(domid)s');"
       
    35     cssclass = None
       
    36     cssstyle = None
       
    37     cwtarget = None
       
    38     redirect_path = None
       
    39     set_error_url = True
       
    40     copy_nav_params = False
       
    41     form_buttons = None # form buttons (button widgets instances)
       
    42     form_renderer_id = 'default'
       
    43 
       
    44     def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
       
    45                  **kwargs):
       
    46         super(FieldsForm, self).__init__(req, rset, row=row, col=col)
       
    47         self.fields = list(self.__class__._fields_)
       
    48         for key, val in kwargs.items():
       
    49             if key in NAV_FORM_PARAMETERS:
       
    50                 self.form_add_hidden(key, val)
       
    51             else:
       
    52                 assert hasattr(self.__class__, key) and not key[0] == '_', key
       
    53                 setattr(self, key, val)
       
    54         if self.set_error_url:
       
    55             self.form_add_hidden('__errorurl', self.session_key())
       
    56         if self.copy_nav_params:
       
    57             for param in NAV_FORM_PARAMETERS:
       
    58                 if not param in kwargs:
       
    59                     value = req.form.get(param)
       
    60                     if value:
       
    61                         self.form_add_hidden(param, value)
       
    62         if submitmsg is not None:
       
    63             self.form_add_hidden('__message', submitmsg)
       
    64         self.context = None
       
    65         if 'domid' in kwargs:# session key changed
       
    66             self.restore_previous_post(self.session_key())
       
    67 
       
    68     @iclassmethod
       
    69     def _fieldsattr(cls_or_self):
       
    70         if isinstance(cls_or_self, type):
       
    71             fields = cls_or_self._fields_
       
    72         else:
       
    73             fields = cls_or_self.fields
       
    74         return fields
       
    75 
       
    76     @iclassmethod
       
    77     def field_by_name(cls_or_self, name, role='subject'):
       
    78         """return field with the given name and role.
       
    79         Raise FieldNotFound if the field can't be found.
       
    80         """
       
    81         for field in cls_or_self._fieldsattr():
       
    82             if field.name == name and field.role == role:
       
    83                 return field
       
    84         raise form.FieldNotFound(name)
       
    85 
       
    86     @iclassmethod
       
    87     def fields_by_name(cls_or_self, name, role='subject'):
       
    88         """return a list of fields with the given name and role"""
       
    89         return [field for field in cls_or_self._fieldsattr()
       
    90                 if field.name == name and field.role == role]
       
    91 
       
    92     @iclassmethod
       
    93     def remove_field(cls_or_self, field):
       
    94         """remove a field from form class or instance"""
       
    95         cls_or_self._fieldsattr().remove(field)
       
    96 
       
    97     @iclassmethod
       
    98     def append_field(cls_or_self, field):
       
    99         """append a field to form class or instance"""
       
   100         cls_or_self._fieldsattr().append(field)
       
   101 
       
   102     @iclassmethod
       
   103     def insert_field_before(cls_or_self, new_field, name, role='subject'):
       
   104         field = cls_or_self.field_by_name(name, role)
       
   105         fields = cls_or_self._fieldsattr()
       
   106         fields.insert(fields.index(field), new_field)
       
   107 
       
   108     @iclassmethod
       
   109     def insert_field_after(cls_or_self, new_field, name, role='subject'):
       
   110         field = cls_or_self.field_by_name(name, role)
       
   111         fields = cls_or_self._fieldsattr()
       
   112         fields.insert(fields.index(field)+1, new_field)
       
   113 
       
   114     @property
       
   115     def form_needs_multipart(self):
       
   116         """true if the form needs enctype=multipart/form-data"""
       
   117         return any(field.needs_multipart for field in self.fields)
       
   118 
       
   119     def form_add_hidden(self, name, value=None, **kwargs):
       
   120         """add an hidden field to the form"""
       
   121         field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
       
   122                             **kwargs)
       
   123         if 'id' in kwargs:
       
   124             # by default, hidden input don't set id attribute. If one is
       
   125             # explicitly specified, ensure it will be set
       
   126             field.widget.setdomid = True
       
   127         self.append_field(field)
       
   128         return field
       
   129 
       
   130     def add_media(self):
       
   131         """adds media (CSS & JS) required by this widget"""
       
   132         if self.needs_js:
       
   133             self.req.add_js(self.needs_js)
       
   134         if self.needs_css:
       
   135             self.req.add_css(self.needs_css)
       
   136 
       
   137     def form_render(self, **values):
       
   138         """render this form, using the renderer given in args or the default
       
   139         FormRenderer()
       
   140         """
       
   141         renderer = values.pop('renderer', None)
       
   142         if renderer is None:
       
   143             renderer = self.form_default_renderer()
       
   144         return renderer.render(self, values)
       
   145 
       
   146     def form_default_renderer(self):
       
   147         return self.vreg.select_object('formrenderers', self.form_renderer_id,
       
   148                                        self.req, self.rset,
       
   149                                        row=self.row, col=self.col)
       
   150 
       
   151     def form_build_context(self, rendervalues=None):
       
   152         """build form context values (the .context attribute which is a
       
   153         dictionary with field instance as key associated to a dictionary
       
   154         containing field 'name' (qualified), 'id', 'value' (for display, always
       
   155         a string).
       
   156 
       
   157         rendervalues is an optional dictionary containing extra kwargs given to
       
   158         form_render()
       
   159         """
       
   160         self.context = context = {}
       
   161         # ensure rendervalues is a dict
       
   162         if rendervalues is None:
       
   163             rendervalues = {}
       
   164         # use a copy in case fields are modified while context is build (eg
       
   165         # __linkto handling for instance)
       
   166         for field in self.fields[:]:
       
   167             for field in field.actual_fields(self):
       
   168                 field.form_init(self)
       
   169                 value = self.form_field_display_value(field, rendervalues)
       
   170                 context[field] = {'value': value,
       
   171                                   'name': self.form_field_name(field),
       
   172                                   'id': self.form_field_id(field),
       
   173                                   }
       
   174 
       
   175     def form_field_display_value(self, field, rendervalues, load_bytes=False):
       
   176         """return field's *string* value to use for display
       
   177 
       
   178         looks in
       
   179         1. previously submitted form values if any (eg on validation error)
       
   180         2. req.form
       
   181         3. extra kw args given to render_form
       
   182         4. field's typed value
       
   183 
       
   184         values found in 1. and 2. are expected te be already some 'display'
       
   185         value while those found in 3. and 4. are expected to be correctly typed.
       
   186         """
       
   187         value = self._req_display_value(field)
       
   188         if value is None:
       
   189             if field.name in rendervalues:
       
   190                 value = rendervalues[field.name]
       
   191             else:
       
   192                 value = self.form_field_value(field, load_bytes)
       
   193                 if callable(value):
       
   194                     value = value(self)
       
   195             if value != INTERNAL_FIELD_VALUE:
       
   196                 value = field.format_value(self.req, value)
       
   197         return value
       
   198 
       
   199     def _req_display_value(self, field):
       
   200         qname = self.form_field_name(field)
       
   201         if qname in self.form_previous_values:
       
   202             return self.form_previous_values[qname]
       
   203         if qname in self.req.form:
       
   204             return self.req.form[qname]
       
   205         if field.name in self.req.form:
       
   206             return self.req.form[field.name]
       
   207         return None
       
   208 
       
   209     def form_field_value(self, field, load_bytes=False):
       
   210         """return field's *typed* value"""
       
   211         myattr = '%s_%s_default' % (field.role, field.name)
       
   212         if hasattr(self, myattr):
       
   213             return getattr(self, myattr)()
       
   214         value = field.initial
       
   215         if callable(value):
       
   216             value = value(self)
       
   217         return value
       
   218 
       
   219     def form_field_error(self, field):
       
   220         """return validation error for widget's field, if any"""
       
   221         if self._field_has_error(field):
       
   222             self.form_displayed_errors.add(field.name)
       
   223             return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
       
   224         return u''
       
   225 
       
   226     def form_field_format(self, field):
       
   227         """return MIME type used for the given (text or bytes) field"""
       
   228         return self.req.property_value('ui.default-text-format')
       
   229 
       
   230     def form_field_encoding(self, field):
       
   231         """return encoding used for the given (text) field"""
       
   232         return self.req.encoding
       
   233 
       
   234     def form_field_name(self, field):
       
   235         """return qualified name for the given field"""
       
   236         return field.name
       
   237 
       
   238     def form_field_id(self, field):
       
   239         """return dom id for the given field"""
       
   240         return field.id
       
   241 
       
   242     def form_field_vocabulary(self, field, limit=None):
       
   243         """return vocabulary for the given field. Should be overriden in
       
   244         specific forms using fields which requires some vocabulary
       
   245         """
       
   246         raise NotImplementedError
       
   247 
       
   248     def _field_has_error(self, field):
       
   249         """return true if the field has some error in given validation exception
       
   250         """
       
   251         return self.form_valerror and field.name in self.form_valerror.errors
       
   252 
       
   253 
       
   254 class EntityFieldsForm(FieldsForm):
       
   255     id = 'base'
       
   256     __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
       
   257 
       
   258     internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
       
   259     domid = 'entityForm'
       
   260 
       
   261     def __init__(self, *args, **kwargs):
       
   262         self.edited_entity = kwargs.pop('entity', None)
       
   263         msg = kwargs.pop('submitmsg', None)
       
   264         super(EntityFieldsForm, self).__init__(*args, **kwargs)
       
   265         if self.edited_entity is None:
       
   266             self.edited_entity = self.complete_entity(self.row or 0, self.col or 0)
       
   267         self.form_add_hidden('__type', eidparam=True)
       
   268         self.form_add_hidden('eid')
       
   269         if msg:
       
   270             # If we need to directly attach the new object to another one
       
   271             self.form_add_hidden('__message', msg)
       
   272         if not self.is_subform:
       
   273             for linkto in self.req.list_form_param('__linkto'):
       
   274                 self.form_add_hidden('__linkto', linkto)
       
   275                 msg = '%s %s' % (msg, self.req._('and linked'))
       
   276 
       
   277     def _field_has_error(self, field):
       
   278         """return true if the field has some error in given validation exception
       
   279         """
       
   280         return super(EntityFieldsForm, self)._field_has_error(field) \
       
   281                and self.form_valerror.eid == self.edited_entity.eid
       
   282 
       
   283     def _relation_vocabulary(self, rtype, targettype, role,
       
   284                             limit=None, done=None):
       
   285         """return unrelated entities for a given relation and target entity type
       
   286         for use in vocabulary
       
   287         """
       
   288         if done is None:
       
   289             done = set()
       
   290         rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
       
   291         res = []
       
   292         for entity in rset.entities():
       
   293             if entity.eid in done:
       
   294                 continue
       
   295             done.add(entity.eid)
       
   296             res.append((entity.view('combobox'), entity.eid))
       
   297         return res
       
   298 
       
   299     def _req_display_value(self, field):
       
   300         value = super(EntityFieldsForm, self)._req_display_value(field)
       
   301         if value is None:
       
   302             value = self.edited_entity.linked_to(field.name, field.role)
       
   303             if value:
       
   304                 searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
       
   305                                   for eid in value]
       
   306                 # remove associated __linkto hidden fields
       
   307                 for field in self.fields_by_name('__linkto'):
       
   308                     if field.initial in searchedvalues:
       
   309                         self.remove_field(field)
       
   310             else:
       
   311                 value = None
       
   312         return value
       
   313 
       
   314     def _form_field_default_value(self, field, load_bytes):
       
   315         defaultattr = 'default_%s' % field.name
       
   316         if hasattr(self.edited_entity, defaultattr):
       
   317             # XXX bw compat, default_<field name> on the entity
       
   318             warn('found %s on %s, should be set on a specific form'
       
   319                  % (defaultattr, self.edited_entity.id), DeprecationWarning)
       
   320             value = getattr(self.edited_entity, defaultattr)
       
   321             if callable(value):
       
   322                 value = value()
       
   323         else:
       
   324             value = super(EntityFieldsForm, self).form_field_value(field,
       
   325                                                                    load_bytes)
       
   326         return value
       
   327 
       
   328     def form_default_renderer(self):
       
   329         return self.vreg.select_object('formrenderers', self.form_renderer_id,
       
   330                                        self.req, self.rset,
       
   331                                        row=self.row, col=self.col,
       
   332                                        entity=self.edited_entity)
       
   333 
       
   334     def form_build_context(self, values=None):
       
   335         """overriden to add edit[s|o] hidden fields and to ensure schema fields
       
   336         have eidparam set to True
       
   337 
       
   338         edit[s|o] hidden fields are used to indicate the value for the
       
   339         associated field before the (potential) modification made when
       
   340         submitting the form.
       
   341         """
       
   342         eschema = self.edited_entity.e_schema
       
   343         for field in self.fields[:]:
       
   344             for field in field.actual_fields(self):
       
   345                 fieldname = field.name
       
   346                 if fieldname != 'eid' and (
       
   347                     (eschema.has_subject_relation(fieldname) or
       
   348                      eschema.has_object_relation(fieldname))):
       
   349                     field.eidparam = True
       
   350                     self.fields.append(HiddenInitialValueField(field))
       
   351         return super(EntityFieldsForm, self).form_build_context(values)
       
   352 
       
   353     def form_field_value(self, field, load_bytes=False):
       
   354         """return field's *typed* value
       
   355 
       
   356         overriden to deal with
       
   357         * special eid / __type / edits- / edito- fields
       
   358         * lookup for values on edited entities
       
   359         """
       
   360         attr = field.name
       
   361         entity = self.edited_entity
       
   362         if attr == 'eid':
       
   363             return entity.eid
       
   364         if not field.eidparam:
       
   365             return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
       
   366         if attr.startswith('edits-') or attr.startswith('edito-'):
       
   367             # edit[s|o]- fieds must have the actual value stored on the entity
       
   368             assert hasattr(field, 'visible_field')
       
   369             vfield = field.visible_field
       
   370             assert vfield.eidparam
       
   371             if entity.has_eid():
       
   372                 return self.form_field_value(vfield)
       
   373             return INTERNAL_FIELD_VALUE
       
   374         if attr == '__type':
       
   375             return entity.id
       
   376         if self.schema.rschema(attr).is_final():
       
   377             attrtype = entity.e_schema.destination(attr)
       
   378             if attrtype == 'Password':
       
   379                 return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
       
   380             if attrtype == 'Bytes':
       
   381                 if entity.has_eid():
       
   382                     if load_bytes:
       
   383                         return getattr(entity, attr)
       
   384                     # XXX value should reflect if some file is already attached
       
   385                     return True
       
   386                 return False
       
   387             if entity.has_eid() or attr in entity:
       
   388                 value = getattr(entity, attr)
       
   389             else:
       
   390                 value = self._form_field_default_value(field, load_bytes)
       
   391             return value
       
   392         # non final relation field
       
   393         if entity.has_eid() or entity.relation_cached(attr, field.role):
       
   394             value = [r[0] for r in entity.related(attr, field.role)]
       
   395         else:
       
   396             value = self._form_field_default_value(field, load_bytes)
       
   397         return value
       
   398 
       
   399     def form_field_format(self, field):
       
   400         """return MIME type used for the given (text or bytes) field"""
       
   401         entity = self.edited_entity
       
   402         if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
       
   403             entity.has_eid() or '%s_format' % field.name in entity):
       
   404             return self.edited_entity.attr_metadata(field.name, 'format')
       
   405         return self.req.property_value('ui.default-text-format')
       
   406 
       
   407     def form_field_encoding(self, field):
       
   408         """return encoding used for the given (text) field"""
       
   409         entity = self.edited_entity
       
   410         if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
       
   411             entity.has_eid() or '%s_encoding' % field.name in entity):
       
   412             return self.edited_entity.attr_metadata(field.name, 'encoding')
       
   413         return super(EntityFieldsForm, self).form_field_encoding(field)
       
   414 
       
   415     def form_field_name(self, field):
       
   416         """return qualified name for the given field"""
       
   417         if field.eidparam:
       
   418             return eid_param(field.name, self.edited_entity.eid)
       
   419         return field.name
       
   420 
       
   421     def form_field_id(self, field):
       
   422         """return dom id for the given field"""
       
   423         if field.eidparam:
       
   424             return eid_param(field.id, self.edited_entity.eid)
       
   425         return field.id
       
   426 
       
   427     def form_field_vocabulary(self, field, limit=None):
       
   428         """return vocabulary for the given field"""
       
   429         role, rtype = field.role, field.name
       
   430         method = '%s_%s_vocabulary' % (role, rtype)
       
   431         try:
       
   432             vocabfunc = getattr(self, method)
       
   433         except AttributeError:
       
   434             try:
       
   435                 # XXX bw compat, <role>_<rtype>_vocabulary on the entity
       
   436                 vocabfunc = getattr(self.edited_entity, method)
       
   437             except AttributeError:
       
   438                 vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
       
   439             else:
       
   440                 warn('found %s on %s, should be set on a specific form'
       
   441                      % (method, self.edited_entity.id), DeprecationWarning)
       
   442         # NOTE: it is the responsibility of `vocabfunc` to sort the result
       
   443         #       (direclty through RQL or via a python sort). This is also
       
   444         #       important because `vocabfunc` might return a list with
       
   445         #       couples (label, None) which act as separators. In these
       
   446         #       cases, it doesn't make sense to sort results afterwards.
       
   447         return vocabfunc(rtype, limit)
       
   448 
       
   449     def subject_relation_vocabulary(self, rtype, limit=None):
       
   450         """defaut vocabulary method for the given relation, looking for
       
   451         relation's object entities (i.e. self is the subject)
       
   452         """
       
   453         entity = self.edited_entity
       
   454         if isinstance(rtype, basestring):
       
   455             rtype = entity.schema.rschema(rtype)
       
   456         done = None
       
   457         assert not rtype.is_final(), rtype
       
   458         if entity.has_eid():
       
   459             done = set(e.eid for e in getattr(entity, str(rtype)))
       
   460         result = []
       
   461         rsetsize = None
       
   462         for objtype in rtype.objects(entity.e_schema):
       
   463             if limit is not None:
       
   464                 rsetsize = limit - len(result)
       
   465             result += self._relation_vocabulary(rtype, objtype, 'subject',
       
   466                                                 rsetsize, done)
       
   467             if limit is not None and len(result) >= limit:
       
   468                 break
       
   469         return result
       
   470 
       
   471     def object_relation_vocabulary(self, rtype, limit=None):
       
   472         """defaut vocabulary method for the given relation, looking for
       
   473         relation's subject entities (i.e. self is the object)
       
   474         """
       
   475         entity = self.edited_entity
       
   476         if isinstance(rtype, basestring):
       
   477             rtype = entity.schema.rschema(rtype)
       
   478         done = None
       
   479         if entity.has_eid():
       
   480             done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
       
   481         result = []
       
   482         rsetsize = None
       
   483         for subjtype in rtype.subjects(entity.e_schema):
       
   484             if limit is not None:
       
   485                 rsetsize = limit - len(result)
       
   486             result += self._relation_vocabulary(rtype, subjtype, 'object',
       
   487                                                 rsetsize, done)
       
   488             if limit is not None and len(result) >= limit:
       
   489                 break
       
   490         return result
       
   491 
       
   492     def subject_in_state_vocabulary(self, rtype, limit=None):
       
   493         """vocabulary method for the in_state relation, looking for relation's
       
   494         object entities (i.e. self is the subject) according to initial_state,
       
   495         state_of and next_state relation
       
   496         """
       
   497         entity = self.edited_entity
       
   498         if not entity.has_eid() or not entity.in_state:
       
   499             # get the initial state
       
   500             rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
       
   501             rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
       
   502             if rset:
       
   503                 return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
       
   504             return []
       
   505         results = []
       
   506         for tr in entity.in_state[0].transitions(entity):
       
   507             state = tr.destination_state[0]
       
   508             results.append((state.view('combobox'), state.eid))
       
   509         return sorted(results)
       
   510 
       
   511 
       
   512 class CompositeForm(FieldsForm):
       
   513     """form composed for sub-forms"""
       
   514     id = 'composite'
       
   515     form_renderer_id = id
       
   516 
       
   517     def __init__(self, *args, **kwargs):
       
   518         super(CompositeForm, self).__init__(*args, **kwargs)
       
   519         self.forms = []
       
   520 
       
   521     def form_add_subform(self, subform):
       
   522         """mark given form as a subform and append it"""
       
   523         subform.is_subform = True
       
   524         self.forms.append(subform)