web/form.py
changeset 2005 e8032965f37a
parent 1995 ec95eaa2b711
child 2656 a93ae0f6c0ad
equal deleted inserted replaced
2004:ea9eab290dcd 2005:e8032965f37a
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
     7 """
     7 """
     8 __docformat__ = "restructuredtext en"
     8 __docformat__ = "restructuredtext en"
     9 
     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.appobject import AppRsetObject
    10 from cubicweb.appobject import AppRsetObject
    16 from cubicweb.selectors import yes, non_final_entity, match_kwargs, one_line_rset
       
    17 from cubicweb.view import NOINDEX, NOFOLLOW
    11 from cubicweb.view import NOINDEX, NOFOLLOW
    18 from cubicweb.common import tags
    12 from cubicweb.common import tags
    19 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
    13 from cubicweb.web import stdmsgs, httpcache, formfields
    20 from cubicweb.web import formwidgets as fwdgs, httpcache
       
    21 from cubicweb.web.controller import NAV_FORM_PARAMETERS
       
    22 from cubicweb.web.formfields import (Field, StringField, RelationField,
       
    23                                      HiddenInitialValueField)
       
    24 
    14 
    25 
    15 
    26 class FormViewMixIn(object):
    16 class FormViewMixIn(object):
    27     """abstract form view mix-in"""
    17     """abstract form view mix-in"""
    28     category = 'form'
    18     category = 'form'
   196         allfields = []
   186         allfields = []
   197         for base in bases:
   187         for base in bases:
   198             if hasattr(base, '_fields_'):
   188             if hasattr(base, '_fields_'):
   199                 allfields += base._fields_
   189                 allfields += base._fields_
   200         clsfields = (item for item in classdict.items()
   190         clsfields = (item for item in classdict.items()
   201                      if isinstance(item[1], Field))
   191                      if isinstance(item[1], formfields.Field))
   202         for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
   192         for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
   203             if not field.name:
   193             if not field.name:
   204                 field.set_name(fieldname)
   194                 field.set_name(fieldname)
   205             allfields.append(field)
   195             allfields.append(field)
   206         classdict['_fields_'] = allfields
   196         classdict['_fields_'] = allfields
   210 class FieldNotFound(Exception):
   200 class FieldNotFound(Exception):
   211     """raised by field_by_name when a field with the given name has not been
   201     """raised by field_by_name when a field with the given name has not been
   212     found
   202     found
   213     """
   203     """
   214 
   204 
   215 class FieldsForm(FormMixIn, AppRsetObject):
   205 class Form(FormMixIn, AppRsetObject):
   216     __metaclass__ = metafieldsform
   206     __metaclass__ = metafieldsform
   217     __registry__ = 'forms'
   207     __registry__ = 'forms'
   218     __select__ = yes()
       
   219 
       
   220     is_subform = False
       
   221 
       
   222     # attributes overrideable through __init__
       
   223     internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
       
   224     needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
       
   225     needs_css = ('cubicweb.form.css',)
       
   226     domid = 'form'
       
   227     title = None
       
   228     action = None
       
   229     onsubmit = "return freezeFormButtons('%(domid)s');"
       
   230     cssclass = None
       
   231     cssstyle = None
       
   232     cwtarget = None
       
   233     redirect_path = None
       
   234     set_error_url = True
       
   235     copy_nav_params = False
       
   236     form_buttons = None # form buttons (button widgets instances)
       
   237     form_renderer_id = 'default'
       
   238 
       
   239     def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
       
   240                  **kwargs):
       
   241         super(FieldsForm, self).__init__(req, rset, row=row, col=col)
       
   242         self.fields = list(self.__class__._fields_)
       
   243         for key, val in kwargs.items():
       
   244             if key in NAV_FORM_PARAMETERS:
       
   245                 self.form_add_hidden(key, val)
       
   246             else:
       
   247                 assert hasattr(self.__class__, key) and not key[0] == '_', key
       
   248                 setattr(self, key, val)
       
   249         if self.set_error_url:
       
   250             self.form_add_hidden('__errorurl', self.session_key())
       
   251         if self.copy_nav_params:
       
   252             for param in NAV_FORM_PARAMETERS:
       
   253                 if not param in kwargs:
       
   254                     value = req.form.get(param)
       
   255                     if value:
       
   256                         self.form_add_hidden(param, value)
       
   257         if submitmsg is not None:
       
   258             self.form_add_hidden('__message', submitmsg)
       
   259         self.context = None
       
   260         if 'domid' in kwargs:# session key changed
       
   261             self.restore_previous_post(self.session_key())
       
   262 
       
   263     @iclassmethod
       
   264     def _fieldsattr(cls_or_self):
       
   265         if isinstance(cls_or_self, type):
       
   266             fields = cls_or_self._fields_
       
   267         else:
       
   268             fields = cls_or_self.fields
       
   269         return fields
       
   270 
       
   271     @iclassmethod
       
   272     def field_by_name(cls_or_self, name, role='subject'):
       
   273         """return field with the given name and role.
       
   274         Raise FieldNotFound if the field can't be found.
       
   275         """
       
   276         for field in cls_or_self._fieldsattr():
       
   277             if field.name == name and field.role == role:
       
   278                 return field
       
   279         raise FieldNotFound(name)
       
   280 
       
   281     @iclassmethod
       
   282     def fields_by_name(cls_or_self, name, role='subject'):
       
   283         """return a list of fields with the given name and role"""
       
   284         return [field for field in cls_or_self._fieldsattr()
       
   285                 if field.name == name and field.role == role]
       
   286 
       
   287     @iclassmethod
       
   288     def remove_field(cls_or_self, field):
       
   289         """remove a field from form class or instance"""
       
   290         cls_or_self._fieldsattr().remove(field)
       
   291 
       
   292     @iclassmethod
       
   293     def append_field(cls_or_self, field):
       
   294         """append a field to form class or instance"""
       
   295         cls_or_self._fieldsattr().append(field)
       
   296 
       
   297     @iclassmethod
       
   298     def insert_field_before(cls_or_self, new_field, name, role='subject'):
       
   299         field = cls_or_self.field_by_name(name, role)
       
   300         fields = cls_or_self._fieldsattr()
       
   301         fields.insert(fields.index(field), new_field)
       
   302 
       
   303     @iclassmethod
       
   304     def insert_field_after(cls_or_self, new_field, name, role='subject'):
       
   305         field = cls_or_self.field_by_name(name, role)
       
   306         fields = cls_or_self._fieldsattr()
       
   307         fields.insert(fields.index(field)+1, new_field)
       
   308 
       
   309     @property
       
   310     def form_needs_multipart(self):
       
   311         """true if the form needs enctype=multipart/form-data"""
       
   312         return any(field.needs_multipart for field in self.fields)
       
   313 
       
   314     def form_add_hidden(self, name, value=None, **kwargs):
       
   315         """add an hidden field to the form"""
       
   316         field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
       
   317                             **kwargs)
       
   318         if 'id' in kwargs:
       
   319             # by default, hidden input don't set id attribute. If one is
       
   320             # explicitly specified, ensure it will be set
       
   321             field.widget.setdomid = True
       
   322         self.append_field(field)
       
   323         return field
       
   324 
       
   325     def add_media(self):
       
   326         """adds media (CSS & JS) required by this widget"""
       
   327         if self.needs_js:
       
   328             self.req.add_js(self.needs_js)
       
   329         if self.needs_css:
       
   330             self.req.add_css(self.needs_css)
       
   331 
       
   332     def form_render(self, **values):
       
   333         """render this form, using the renderer given in args or the default
       
   334         FormRenderer()
       
   335         """
       
   336         renderer = values.pop('renderer', None)
       
   337         if renderer is None:
       
   338             renderer = self.form_default_renderer()
       
   339         return renderer.render(self, values)
       
   340 
       
   341     def form_default_renderer(self):
       
   342         return self.vreg.select_object('formrenderers', self.form_renderer_id,
       
   343                                        self.req, self.rset,
       
   344                                        row=self.row, col=self.col)
       
   345 
       
   346     def form_build_context(self, rendervalues=None):
       
   347         """build form context values (the .context attribute which is a
       
   348         dictionary with field instance as key associated to a dictionary
       
   349         containing field 'name' (qualified), 'id', 'value' (for display, always
       
   350         a string).
       
   351 
       
   352         rendervalues is an optional dictionary containing extra kwargs given to
       
   353         form_render()
       
   354         """
       
   355         self.context = context = {}
       
   356         # ensure rendervalues is a dict
       
   357         if rendervalues is None:
       
   358             rendervalues = {}
       
   359         # use a copy in case fields are modified while context is build (eg
       
   360         # __linkto handling for instance)
       
   361         for field in self.fields[:]:
       
   362             for field in field.actual_fields(self):
       
   363                 field.form_init(self)
       
   364                 value = self.form_field_display_value(field, rendervalues)
       
   365                 context[field] = {'value': value,
       
   366                                   'name': self.form_field_name(field),
       
   367                                   'id': self.form_field_id(field),
       
   368                                   }
       
   369 
       
   370     def form_field_display_value(self, field, rendervalues, load_bytes=False):
       
   371         """return field's *string* value to use for display
       
   372 
       
   373         looks in
       
   374         1. previously submitted form values if any (eg on validation error)
       
   375         2. req.form
       
   376         3. extra kw args given to render_form
       
   377         4. field's typed value
       
   378 
       
   379         values found in 1. and 2. are expected te be already some 'display'
       
   380         value while those found in 3. and 4. are expected to be correctly typed.
       
   381         """
       
   382         value = self._req_display_value(field)
       
   383         if value is None:
       
   384             if field.name in rendervalues:
       
   385                 value = rendervalues[field.name]
       
   386             else:
       
   387                 value = self.form_field_value(field, load_bytes)
       
   388                 if callable(value):
       
   389                     value = value(self)
       
   390             if value != INTERNAL_FIELD_VALUE:
       
   391                 value = field.format_value(self.req, value)
       
   392         return value
       
   393 
       
   394     def _req_display_value(self, field):
       
   395         qname = self.form_field_name(field)
       
   396         if qname in self.form_previous_values:
       
   397             return self.form_previous_values[qname]
       
   398         if qname in self.req.form:
       
   399             return self.req.form[qname]
       
   400         if field.name in self.req.form:
       
   401             return self.req.form[field.name]
       
   402         return None
       
   403 
       
   404     def form_field_value(self, field, load_bytes=False):
       
   405         """return field's *typed* value"""
       
   406         myattr = '%s_%s_default' % (field.role, field.name)
       
   407         if hasattr(self, myattr):
       
   408             return getattr(self, myattr)()
       
   409         value = field.initial
       
   410         if callable(value):
       
   411             value = value(self)
       
   412         return value
       
   413 
       
   414     def form_field_error(self, field):
       
   415         """return validation error for widget's field, if any"""
       
   416         if self._field_has_error(field):
       
   417             self.form_displayed_errors.add(field.name)
       
   418             return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
       
   419         return u''
       
   420 
       
   421     def form_field_format(self, field):
       
   422         """return MIME type used for the given (text or bytes) field"""
       
   423         return self.req.property_value('ui.default-text-format')
       
   424 
       
   425     def form_field_encoding(self, field):
       
   426         """return encoding used for the given (text) field"""
       
   427         return self.req.encoding
       
   428 
       
   429     def form_field_name(self, field):
       
   430         """return qualified name for the given field"""
       
   431         return field.name
       
   432 
       
   433     def form_field_id(self, field):
       
   434         """return dom id for the given field"""
       
   435         return field.id
       
   436 
       
   437     def form_field_vocabulary(self, field, limit=None):
       
   438         """return vocabulary for the given field. Should be overriden in
       
   439         specific forms using fields which requires some vocabulary
       
   440         """
       
   441         raise NotImplementedError
       
   442 
       
   443     def _field_has_error(self, field):
       
   444         """return true if the field has some error in given validation exception
       
   445         """
       
   446         return self.form_valerror and field.name in self.form_valerror.errors
       
   447 
       
   448 
       
   449 class EntityFieldsForm(FieldsForm):
       
   450     __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
       
   451 
       
   452     internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
       
   453     domid = 'entityForm'
       
   454 
       
   455     def __init__(self, *args, **kwargs):
       
   456         self.edited_entity = kwargs.pop('entity', None)
       
   457         msg = kwargs.pop('submitmsg', None)
       
   458         super(EntityFieldsForm, self).__init__(*args, **kwargs)
       
   459         if self.edited_entity is None:
       
   460             self.edited_entity = self.complete_entity(self.row or 0, self.col or 0)
       
   461         self.form_add_hidden('__type', eidparam=True)
       
   462         self.form_add_hidden('eid')
       
   463         if msg:
       
   464             # If we need to directly attach the new object to another one
       
   465             self.form_add_hidden('__message', msg)
       
   466         if not self.is_subform:
       
   467             for linkto in self.req.list_form_param('__linkto'):
       
   468                 self.form_add_hidden('__linkto', linkto)
       
   469                 msg = '%s %s' % (msg, self.req._('and linked'))
       
   470         # in case of direct instanciation
       
   471         self.schema = self.edited_entity.schema
       
   472         self.vreg = self.edited_entity.vreg
       
   473 
       
   474     def _field_has_error(self, field):
       
   475         """return true if the field has some error in given validation exception
       
   476         """
       
   477         return super(EntityFieldsForm, self)._field_has_error(field) \
       
   478                and self.form_valerror.eid == self.edited_entity.eid
       
   479 
       
   480     def _relation_vocabulary(self, rtype, targettype, role,
       
   481                             limit=None, done=None):
       
   482         """return unrelated entities for a given relation and target entity type
       
   483         for use in vocabulary
       
   484         """
       
   485         if done is None:
       
   486             done = set()
       
   487         rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
       
   488         res = []
       
   489         for entity in rset.entities():
       
   490             if entity.eid in done:
       
   491                 continue
       
   492             done.add(entity.eid)
       
   493             res.append((entity.view('combobox'), entity.eid))
       
   494         return res
       
   495 
       
   496     def _req_display_value(self, field):
       
   497         value = super(EntityFieldsForm, self)._req_display_value(field)
       
   498         if value is None:
       
   499             value = self.edited_entity.linked_to(field.name, field.role)
       
   500             if value:
       
   501                 searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
       
   502                                   for eid in value]
       
   503                 # remove associated __linkto hidden fields
       
   504                 for field in self.fields_by_name('__linkto'):
       
   505                     if field.initial in searchedvalues:
       
   506                         self.remove_field(field)
       
   507             else:
       
   508                 value = None
       
   509         return value
       
   510 
       
   511     def _form_field_default_value(self, field, load_bytes):
       
   512         defaultattr = 'default_%s' % field.name
       
   513         if hasattr(self.edited_entity, defaultattr):
       
   514             # XXX bw compat, default_<field name> on the entity
       
   515             warn('found %s on %s, should be set on a specific form'
       
   516                  % (defaultattr, self.edited_entity.id), DeprecationWarning)
       
   517             value = getattr(self.edited_entity, defaultattr)
       
   518             if callable(value):
       
   519                 value = value()
       
   520         else:
       
   521             value = super(EntityFieldsForm, self).form_field_value(field,
       
   522                                                                    load_bytes)
       
   523         return value
       
   524 
       
   525     def form_default_renderer(self):
       
   526         return self.vreg.select_object('formrenderers', self.form_renderer_id,
       
   527                                        self.req, self.rset,
       
   528                                        row=self.row, col=self.col,
       
   529                                        entity=self.edited_entity)
       
   530 
       
   531     def form_build_context(self, values=None):
       
   532         """overriden to add edit[s|o] hidden fields and to ensure schema fields
       
   533         have eidparam set to True
       
   534 
       
   535         edit[s|o] hidden fields are used to indicate the value for the
       
   536         associated field before the (potential) modification made when
       
   537         submitting the form.
       
   538         """
       
   539         eschema = self.edited_entity.e_schema
       
   540         for field in self.fields[:]:
       
   541             for field in field.actual_fields(self):
       
   542                 fieldname = field.name
       
   543                 if fieldname != 'eid' and (
       
   544                     (eschema.has_subject_relation(fieldname) or
       
   545                      eschema.has_object_relation(fieldname))):
       
   546                     field.eidparam = True
       
   547                     self.fields.append(HiddenInitialValueField(field))
       
   548         return super(EntityFieldsForm, self).form_build_context(values)
       
   549 
       
   550     def form_field_value(self, field, load_bytes=False):
       
   551         """return field's *typed* value
       
   552 
       
   553         overriden to deal with
       
   554         * special eid / __type / edits- / edito- fields
       
   555         * lookup for values on edited entities
       
   556         """
       
   557         attr = field.name
       
   558         entity = self.edited_entity
       
   559         if attr == 'eid':
       
   560             return entity.eid
       
   561         if not field.eidparam:
       
   562             return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
       
   563         if attr.startswith('edits-') or attr.startswith('edito-'):
       
   564             # edit[s|o]- fieds must have the actual value stored on the entity
       
   565             assert hasattr(field, 'visible_field')
       
   566             vfield = field.visible_field
       
   567             assert vfield.eidparam
       
   568             if entity.has_eid():
       
   569                 return self.form_field_value(vfield)
       
   570             return INTERNAL_FIELD_VALUE
       
   571         if attr == '__type':
       
   572             return entity.id
       
   573         if self.schema.rschema(attr).is_final():
       
   574             attrtype = entity.e_schema.destination(attr)
       
   575             if attrtype == 'Password':
       
   576                 return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
       
   577             if attrtype == 'Bytes':
       
   578                 if entity.has_eid():
       
   579                     if load_bytes:
       
   580                         return getattr(entity, attr)
       
   581                     # XXX value should reflect if some file is already attached
       
   582                     return True
       
   583                 return False
       
   584             if entity.has_eid() or attr in entity:
       
   585                 value = getattr(entity, attr)
       
   586             else:
       
   587                 value = self._form_field_default_value(field, load_bytes)
       
   588             return value
       
   589         # non final relation field
       
   590         if entity.has_eid() or entity.relation_cached(attr, field.role):
       
   591             value = [r[0] for r in entity.related(attr, field.role)]
       
   592         else:
       
   593             value = self._form_field_default_value(field, load_bytes)
       
   594         return value
       
   595 
       
   596     def form_field_format(self, field):
       
   597         """return MIME type used for the given (text or bytes) field"""
       
   598         entity = self.edited_entity
       
   599         if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
       
   600             entity.has_eid() or '%s_format' % field.name in entity):
       
   601             return self.edited_entity.attr_metadata(field.name, 'format')
       
   602         return self.req.property_value('ui.default-text-format')
       
   603 
       
   604     def form_field_encoding(self, field):
       
   605         """return encoding used for the given (text) field"""
       
   606         entity = self.edited_entity
       
   607         if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
       
   608             entity.has_eid() or '%s_encoding' % field.name in entity):
       
   609             return self.edited_entity.attr_metadata(field.name, 'encoding')
       
   610         return super(EntityFieldsForm, self).form_field_encoding(field)
       
   611 
       
   612     def form_field_name(self, field):
       
   613         """return qualified name for the given field"""
       
   614         if field.eidparam:
       
   615             return eid_param(field.name, self.edited_entity.eid)
       
   616         return field.name
       
   617 
       
   618     def form_field_id(self, field):
       
   619         """return dom id for the given field"""
       
   620         if field.eidparam:
       
   621             return eid_param(field.id, self.edited_entity.eid)
       
   622         return field.id
       
   623 
       
   624     def form_field_vocabulary(self, field, limit=None):
       
   625         """return vocabulary for the given field"""
       
   626         role, rtype = field.role, field.name
       
   627         method = '%s_%s_vocabulary' % (role, rtype)
       
   628         try:
       
   629             vocabfunc = getattr(self, method)
       
   630         except AttributeError:
       
   631             try:
       
   632                 # XXX bw compat, <role>_<rtype>_vocabulary on the entity
       
   633                 vocabfunc = getattr(self.edited_entity, method)
       
   634             except AttributeError:
       
   635                 vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
       
   636             else:
       
   637                 warn('found %s on %s, should be set on a specific form'
       
   638                      % (method, self.edited_entity.id), DeprecationWarning)
       
   639         # NOTE: it is the responsibility of `vocabfunc` to sort the result
       
   640         #       (direclty through RQL or via a python sort). This is also
       
   641         #       important because `vocabfunc` might return a list with
       
   642         #       couples (label, None) which act as separators. In these
       
   643         #       cases, it doesn't make sense to sort results afterwards.
       
   644         return vocabfunc(rtype, limit)
       
   645 
       
   646     def subject_relation_vocabulary(self, rtype, limit=None):
       
   647         """defaut vocabulary method for the given relation, looking for
       
   648         relation's object entities (i.e. self is the subject)
       
   649         """
       
   650         entity = self.edited_entity
       
   651         if isinstance(rtype, basestring):
       
   652             rtype = entity.schema.rschema(rtype)
       
   653         done = None
       
   654         assert not rtype.is_final(), rtype
       
   655         if entity.has_eid():
       
   656             done = set(e.eid for e in getattr(entity, str(rtype)))
       
   657         result = []
       
   658         rsetsize = None
       
   659         for objtype in rtype.objects(entity.e_schema):
       
   660             if limit is not None:
       
   661                 rsetsize = limit - len(result)
       
   662             result += self._relation_vocabulary(rtype, objtype, 'subject',
       
   663                                                 rsetsize, done)
       
   664             if limit is not None and len(result) >= limit:
       
   665                 break
       
   666         return result
       
   667 
       
   668     def object_relation_vocabulary(self, rtype, limit=None):
       
   669         """defaut vocabulary method for the given relation, looking for
       
   670         relation's subject entities (i.e. self is the object)
       
   671         """
       
   672         entity = self.edited_entity
       
   673         if isinstance(rtype, basestring):
       
   674             rtype = entity.schema.rschema(rtype)
       
   675         done = None
       
   676         if entity.has_eid():
       
   677             done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
       
   678         result = []
       
   679         rsetsize = None
       
   680         for subjtype in rtype.subjects(entity.e_schema):
       
   681             if limit is not None:
       
   682                 rsetsize = limit - len(result)
       
   683             result += self._relation_vocabulary(rtype, subjtype, 'object',
       
   684                                                 rsetsize, done)
       
   685             if limit is not None and len(result) >= limit:
       
   686                 break
       
   687         return result
       
   688 
       
   689     def subject_in_state_vocabulary(self, rtype, limit=None):
       
   690         """vocabulary method for the in_state relation, looking for relation's
       
   691         object entities (i.e. self is the subject) according to initial_state,
       
   692         state_of and next_state relation
       
   693         """
       
   694         entity = self.edited_entity
       
   695         if not entity.has_eid() or not entity.in_state:
       
   696             # get the initial state
       
   697             rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
       
   698             rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
       
   699             if rset:
       
   700                 return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
       
   701             return []
       
   702         results = []
       
   703         for tr in entity.in_state[0].transitions(entity):
       
   704             state = tr.destination_state[0]
       
   705             results.append((state.view('combobox'), state.eid))
       
   706         return sorted(results)
       
   707 
       
   708 
       
   709 class CompositeForm(FieldsForm):
       
   710     """form composed for sub-forms"""
       
   711     form_renderer_id = 'composite'
       
   712 
       
   713     def __init__(self, *args, **kwargs):
       
   714         super(CompositeForm, self).__init__(*args, **kwargs)
       
   715         self.forms = []
       
   716 
       
   717     def form_add_subform(self, subform):
       
   718         """mark given form as a subform and append it"""
       
   719         subform.is_subform = True
       
   720         self.forms.append(subform)