diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/autoform.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/autoform.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1057 @@
+# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see .
+"""
+.. autodocstring:: cubicweb.web.views.autoform::AutomaticEntityForm
+
+Configuration through uicfg
+```````````````````````````
+
+It is possible to manage which and how an entity's attributes and relations
+will be edited in the various contexts where the automatic entity form is used
+by using proper uicfg tags.
+
+The details of the uicfg syntax can be found in the :ref:`uicfg` chapter.
+
+Possible relation tags that apply to entity forms are detailled below.
+They are all in the :mod:`cubicweb.web.uicfg` module.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``autoform_section`` specifies where to display a relation in form for a given
+form type. :meth:`tag_attribute`, :meth:`tag_subject_of` and
+:meth:`tag_object_of` methods for this relation tag expect two arguments
+additionally to the relation key: a `formtype` and a `section`.
+
+`formtype` may be one of:
+
+* 'main', the main entity form (e.g. the one you get when creating or editing an
+ entity)
+
+* 'inlined', the form for an entity inlined into another form
+
+* 'muledit', the table form when editing multiple entities of the same type
+
+
+section may be one of:
+
+* 'hidden', don't display (not even in a hidden input)
+
+* 'attributes', display in the attributes section
+
+* 'relations', display in the relations section, using the generic relation
+ selector combobox (available in main form only, and not usable for attributes)
+
+* 'inlined', display target entity of the relation into an inlined form
+ (available in main form only, and not for attributes)
+
+By default, mandatory relations are displayed in the 'attributes' section,
+others in 'relations' section.
+
+
+Change default fields
+^^^^^^^^^^^^^^^^^^^^^
+
+Use ``autoform_field`` to replace the default field class to use for a relation
+or attribute. You can put either a field class or instance as value (put a class
+whenether it's possible).
+
+.. Warning::
+
+ `autoform_field_kwargs` should usually be used instead of
+ `autoform_field`. If you put a field instance into `autoform_field`,
+ `autoform_field_kwargs` values for this relation will be ignored.
+
+
+Customize field options
+^^^^^^^^^^^^^^^^^^^^^^^
+
+In order to customize field options (see :class:`~cubicweb.web.formfields.Field`
+for a detailed list of options), use `autoform_field_kwargs`. This rtag takes
+a dictionary as arguments, that will be given to the field's contructor.
+
+You can then put in that dictionary any arguments supported by the field
+class. For instance:
+
+.. sourcecode:: python
+
+ # Change the content of the combobox. Here `ticket_done_in_choices` is a
+ # function which returns a list of elements to populate the combobox
+ autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'),
+ {'sort': False,
+ 'choices': ticket_done_in_choices})
+
+ # Force usage of a TextInput widget for the expression attribute of
+ # RQLExpression entities
+ autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'),
+ {'widget': fw.TextInput})
+
+.. note::
+
+ the widget argument can be either a class or an instance (the later
+ case being convenient to pass the Widget specific initialisation
+ options)
+
+Overriding permissions
+^^^^^^^^^^^^^^^^^^^^^^
+
+The `autoform_permissions_overrides` rtag provides a way to by-pass security
+checking for dark-corner case where it can't be verified properly.
+
+
+.. More about inlined forms
+.. Controlling the generic relation fields
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from warnings import warn
+
+from six.moves import range
+
+from logilab.mtconverter import xml_escape
+from logilab.common.decorators import iclassmethod, cached
+from logilab.common.deprecation import deprecated
+from logilab.common.registry import NoSelectableObject
+
+from cubicweb import neg_role, uilib
+from cubicweb.schema import display_name
+from cubicweb.view import EntityView
+from cubicweb.predicates import (
+ match_kwargs, match_form_params, non_final_entity,
+ specified_etype_implements)
+from cubicweb.utils import json_dumps
+from cubicweb.web import (stdmsgs, eid_param,
+ form as f, formwidgets as fw, formfields as ff)
+from cubicweb.web.views import uicfg, forms
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+
+# inlined form handling ########################################################
+
+class InlinedFormField(ff.Field):
+ def __init__(self, view=None, **kwargs):
+ kwargs.setdefault('label', None)
+ # don't add eidparam=True since this field doesn't actually hold the
+ # relation value (the subform does) hence should not be listed in
+ # _cw_entity_fields
+ super(InlinedFormField, self).__init__(name=view.rtype, role=view.role,
+ **kwargs)
+ self.view = view
+
+ def render(self, form, renderer):
+ """render this field, which is part of form, using the given form
+ renderer
+ """
+ view = self.view
+ i18nctx = 'inlined:%s.%s.%s' % (form.edited_entity.e_schema,
+ view.rtype, view.role)
+ return u'
%s
' % (
+ view.rtype, view.role,
+ view.render(i18nctx=i18nctx, row=view.cw_row, col=view.cw_col))
+
+ def form_init(self, form):
+ """method called before by build_context to trigger potential field
+ initialization requiring the form instance
+ """
+ if self.view.form:
+ self.view.form.build_context(form.formvalues)
+
+ @property
+ def needs_multipart(self):
+ if self.view.form:
+ # take a look at inlined forms to check (recursively) if they need
+ # multipart handling.
+ return self.view.form.needs_multipart
+ return False
+
+ def has_been_modified(self, form):
+ return False
+
+ def process_posted(self, form):
+ pass # handled by the subform
+
+
+class InlineEntityEditionFormView(f.FormViewMixIn, EntityView):
+ """
+ :attr peid: the parent entity's eid hosting the inline form
+ :attr rtype: the relation bridging `etype` and `peid`
+ :attr role: the role played by the `peid` in the relation
+ :attr pform: the parent form where this inlined form is being displayed
+ """
+ __regid__ = 'inline-edition'
+ __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
+
+ _select_attrs = ('peid', 'rtype', 'role', 'pform', 'etype')
+ removejs = "removeInlinedEntity('%s', '%s', '%s')"
+
+ # make pylint happy
+ peid = rtype = role = pform = etype = None
+
+ def __init__(self, *args, **kwargs):
+ for attr in self._select_attrs:
+ # don't pop attributes from kwargs, so the end-up in
+ # self.cw_extra_kwargs which is then passed to the edition form (see
+ # the .form method)
+ setattr(self, attr, kwargs.get(attr))
+ super(InlineEntityEditionFormView, self).__init__(*args, **kwargs)
+
+ def _entity(self):
+ assert self.cw_row is not None, self
+ return self.cw_rset.get_entity(self.cw_row, self.cw_col)
+
+ @property
+ def petype(self):
+ assert isinstance(self.peid, int)
+ pentity = self._cw.entity_from_eid(self.peid)
+ return pentity.e_schema.type
+
+ @property
+ @cached
+ def form(self):
+ entity = self._entity()
+ form = self._cw.vreg['forms'].select('edition', self._cw,
+ entity=entity,
+ formtype='inlined',
+ form_renderer_id='inline',
+ copy_nav_params=False,
+ mainform=False,
+ parent_form=self.pform,
+ **self.cw_extra_kwargs)
+ if self.pform is None:
+ form.restore_previous_post(form.session_key())
+ #assert form.parent_form
+ self.add_hiddens(form, entity)
+ return form
+
+ def cell_call(self, row, col, i18nctx, **kwargs):
+ """
+ :param peid: the parent entity's eid hosting the inline form
+ :param rtype: the relation bridging `etype` and `peid`
+ :param role: the role played by the `peid` in the relation
+ """
+ entity = self._entity()
+ divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (
+ self.peid, self.rtype, entity.eid)
+ self.render_form(i18nctx, divonclick=divonclick, **kwargs)
+
+ def _get_removejs(self):
+ """
+ Don't display the remove link in edition form if the
+ cardinality is 1. Handled in InlineEntityCreationFormView for
+ creation form.
+ """
+ entity = self._entity()
+ rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
+ card = rdef.role_cardinality(self.role)
+ if card == '1': # don't display remove link
+ return None
+ # if cardinality is 1..n (+), dont display link to remove an inlined form for the first form
+ # allowing to edit the relation. To detect so:
+ #
+ # * if parent form (pform) is None, we're generated through an ajax call and so we know this
+ # is not the first form
+ #
+ # * if parent form is not None, look for previous InlinedFormField in the parent's form
+ # fields
+ if card == '+' and self.pform is not None:
+ # retrieve all field'views handling this relation and return None if we're the first of
+ # them
+ first_view = next(iter((f.view for f in self.pform.fields
+ if isinstance(f, InlinedFormField)
+ and f.view.rtype == self.rtype and f.view.role == self.role)))
+ if self == first_view:
+ return None
+ return self.removejs and self.removejs % (
+ self.peid, self.rtype, entity.eid)
+
+ def render_form(self, i18nctx, **kwargs):
+ """fetch and render the form"""
+ entity = self._entity()
+ divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid)
+ title = self.form_title(entity, i18nctx)
+ removejs = self._get_removejs()
+ countkey = '%s_count' % self.rtype
+ try:
+ self._cw.data[countkey] += 1
+ except KeyError:
+ self._cw.data[countkey] = 1
+ self.form.render(w=self.w, divid=divid, title=title, removejs=removejs,
+ i18nctx=i18nctx, counter=self._cw.data[countkey] ,
+ **kwargs)
+
+ def form_title(self, entity, i18nctx):
+ return self._cw.pgettext(i18nctx, entity.cw_etype)
+
+ def add_hiddens(self, form, entity):
+ """to ease overriding (see cubes.vcsfile.views.forms for instance)"""
+ iid = 'rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid)
+ # * str(self.rtype) in case it's a schema object
+ # * neged_role() since role is the for parent entity, we want the role
+ # of the inlined entity
+ form.add_hidden(name=str(self.rtype), value=self.peid,
+ role=neg_role(self.role), eidparam=True, id=iid)
+
+ def keep_entity(self, form, entity):
+ if not entity.has_eid():
+ return True
+ # are we regenerating form because of a validation error?
+ if form.form_previous_values:
+ cdvalues = self._cw.list_form_param(eid_param(self.rtype, self.peid),
+ form.form_previous_values)
+ if unicode(entity.eid) not in cdvalues:
+ return False
+ return True
+
+
+class InlineEntityCreationFormView(InlineEntityEditionFormView):
+ """
+ :attr etype: the entity type being created in the inline form
+ """
+ __regid__ = 'inline-creation'
+ __select__ = (match_kwargs('peid', 'petype', 'rtype')
+ & specified_etype_implements('Any'))
+ _select_attrs = InlineEntityEditionFormView._select_attrs + ('petype',)
+
+ # make pylint happy
+ petype = None
+
+ @property
+ def removejs(self):
+ entity = self._entity()
+ rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
+ card = rdef.role_cardinality(self.role)
+ # when one is adding an inline entity for a relation of a single card,
+ # the 'add a new xxx' link disappears. If the user then cancel the addition,
+ # we have to make this link appears back. This is done by giving add new link
+ # id to removeInlineForm.
+ if card == '?':
+ divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
+ return "removeInlineForm('%%s', '%%s', '%s', '%%s', '%s')" % (
+ self.role, divid)
+ elif card in '+*':
+ return "removeInlineForm('%%s', '%%s', '%s', '%%s')" % self.role
+ # don't do anything for card == '1'
+
+ @cached
+ def _entity(self):
+ try:
+ cls = self._cw.vreg['etypes'].etype_class(self.etype)
+ except Exception:
+ self.w(self._cw._('no such entity type %s') % self.etype)
+ return
+ entity = cls(self._cw)
+ entity.eid = next(self._cw.varmaker)
+ return entity
+
+ def call(self, i18nctx, **kwargs):
+ self.render_form(i18nctx, **kwargs)
+
+
+class InlineAddNewLinkView(InlineEntityCreationFormView):
+ """
+ :attr card: the cardinality of the relation according to role of `peid`
+ """
+ __regid__ = 'inline-addnew-link'
+ __select__ = (match_kwargs('peid', 'petype', 'rtype')
+ & specified_etype_implements('Any'))
+
+ _select_attrs = InlineEntityCreationFormView._select_attrs + ('card',)
+ card = None # make pylint happy
+ form = None # no actual form wrapped
+
+ def call(self, i18nctx, **kwargs):
+ self._cw.set_varmaker()
+ divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
+ self.w(u'
+""" % (hidden and 'hidden' or '', divid, selectid,
+ xml_escape(json_dumps(entity.eid)), is_cell and 'true' or 'null', relname,
+ '\n'.join(options))
+
+ def _get_select_options(self, entity, rschema, role):
+ """add options to search among all entities of each possible type"""
+ options = []
+ pending_inserts = get_pending_inserts(self._cw, entity.eid)
+ rtype = rschema.type
+ form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
+ field = form.field_by_name(rschema, role, entity.e_schema)
+ limit = self._cw.property_value('navigation.combobox-limit')
+ # NOTE: expect 'limit' arg on choices method of relation field
+ for eview, reid in field.vocabulary(form, limit=limit):
+ if reid is None:
+ if eview: # skip blank value
+ options.append(''
+ % xml_escape(eview))
+ elif reid != ff.INTERNAL_FIELD_VALUE:
+ optionid = relation_id(entity.eid, rtype, role, reid)
+ if optionid not in pending_inserts:
+ # prefix option's id with letters to make valid XHTML wise
+ options.append('' %
+ (optionid, reid, xml_escape(eview)))
+ return options
+
+ def _get_search_options(self, entity, rschema, role, targettypes):
+ """add options to search among all entities of each possible type"""
+ options = []
+ _ = self._cw._
+ for eschema in targettypes:
+ mode = '%s:%s:%s:%s' % (role, entity.eid, rschema.type, eschema)
+ url = self._cw.build_url(entity.rest_path(), vid='search-associate',
+ __mode=mode)
+ options.append((eschema.display_name(self._cw),
+ '' % (
+ xml_escape(url), _('Search for'), eschema.display_name(self._cw))))
+ return [o for l, o in sorted(options)]
+
+
+# The automatic entity form ####################################################
+
+class AutomaticEntityForm(forms.EntityFieldsForm):
+ """AutomaticEntityForm is an automagic form to edit any entity. It
+ is designed to be fully generated from schema but highly
+ configurable through uicfg.
+
+ Of course, as for other forms, you can also customise it by specifying
+ various standard form parameters on selection, overriding, or
+ adding/removing fields in selected instances.
+ """
+ __regid__ = 'edition'
+
+ cwtarget = 'eformframe'
+ cssclass = 'entityForm'
+ copy_nav_params = True
+ form_buttons = [fw.SubmitButton(),
+ fw.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'),
+ fw.Button(stdmsgs.BUTTON_CANCEL,
+ {'class': fw.Button.css_class + ' cwjs-edition-cancel'})]
+ # for attributes selection when searching in uicfg.autoform_section
+ formtype = 'main'
+ # set this to a list of [(relation, role)] if you want to explictily tell
+ # which relations should be edited
+ display_fields = None
+ # action on the form tag
+ _default_form_action_path = 'validateform'
+
+ @iclassmethod
+ def field_by_name(cls_or_self, name, role=None, eschema=None):
+ """return field with the given name and role. If field is not explicitly
+ defined for the form but `eclass` is specified, guess_field will be
+ called.
+ """
+ try:
+ return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role, eschema)
+ except f.FieldNotFound:
+ if name == '_cw_generic_field' and not isinstance(cls_or_self, type):
+ return cls_or_self._generic_relations_field()
+ raise
+
+ # base automatic entity form methods #######################################
+
+ def __init__(self, *args, **kwargs):
+ super(AutomaticEntityForm, self).__init__(*args, **kwargs)
+ self.uicfg_afs = self._cw.vreg['uicfg'].select(
+ 'autoform_section', self._cw, entity=self.edited_entity)
+ entity = self.edited_entity
+ if entity.has_eid():
+ entity.complete()
+ for rtype, role in self.editable_attributes():
+ try:
+ self.field_by_name(str(rtype), role)
+ continue # explicitly specified
+ except f.FieldNotFound:
+ # has to be guessed
+ try:
+ field = self.field_by_name(str(rtype), role,
+ eschema=entity.e_schema)
+ self.fields.append(field)
+ except f.FieldNotFound:
+ # meta attribute such as _format
+ continue
+ if self.fieldsets_in_order:
+ fsio = list(self.fieldsets_in_order)
+ else:
+ fsio = [None]
+ self.fieldsets_in_order = fsio
+ # add fields for relation whose target should have an inline form
+ for formview in self.inlined_form_views():
+ field = self._inlined_form_view_field(formview)
+ self.fields.append(field)
+ if not field.fieldset in fsio:
+ fsio.append(field.fieldset)
+ if self.formtype == 'main':
+ # add the generic relation field if necessary
+ if entity.has_eid() and (
+ self.display_fields is None or
+ '_cw_generic_field' in self.display_fields):
+ try:
+ field = self.field_by_name('_cw_generic_field')
+ except f.FieldNotFound:
+ # no editable relation
+ pass
+ else:
+ self.fields.append(field)
+ if not field.fieldset in fsio:
+ fsio.append(field.fieldset)
+ self.maxrelitems = self._cw.property_value('navigation.related-limit')
+ self.force_display = bool(self._cw.form.get('__force_display'))
+ fnum = len(self.fields)
+ self.fields.sort(key=lambda f: f.order is None and fnum or f.order)
+
+ @property
+ def related_limit(self):
+ if self.force_display:
+ return None
+ return self.maxrelitems + 1
+
+ # autoform specific fields #################################################
+
+ def _generic_relations_field(self):
+ srels_by_cat = self.editable_relations()
+ if not srels_by_cat:
+ raise f.FieldNotFound('_cw_generic_field')
+ fieldset = 'This %s:' % self.edited_entity.e_schema
+ return GenericRelationsField(self.editable_relations(),
+ fieldset=fieldset, label=None)
+
+ def _inlined_form_view_field(self, view):
+ # XXX allow more customization
+ kwargs = self.uicfg_affk.etype_get(self.edited_entity.e_schema,
+ view.rtype, view.role, view.etype)
+ if kwargs is None:
+ kwargs = {}
+ return InlinedFormField(view=view, **kwargs)
+
+ # methods mapping edited entity relations to fields in the form ############
+
+ def _relations_by_section(self, section, permission='add', strict=False):
+ """return a list of (relation schema, target schemas, role) matching
+ given category(ies) and permission
+ """
+ return self.uicfg_afs.relations_by_section(
+ self.edited_entity, self.formtype, section, permission, strict)
+
+ def editable_attributes(self, strict=False):
+ """return a list of (relation schema, role) to edit for the entity"""
+ if self.display_fields is not None:
+ schema = self._cw.vreg.schema
+ return [(schema[rtype], role) for rtype, role in self.display_fields]
+ if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
+ return []
+ action = 'update' if self.edited_entity.has_eid() else 'add'
+ return [(rtype, role) for rtype, _, role in self._relations_by_section(
+ 'attributes', action, strict)]
+
+ def editable_relations(self):
+ """return a sorted list of (relation's label, relation'schema, role) for
+ relations in the 'relations' section
+ """
+ result = []
+ for rschema, _, role in self._relations_by_section('relations',
+ strict=True):
+ result.append( (rschema.display_name(self.edited_entity._cw, role,
+ self.edited_entity.cw_etype),
+ rschema, role) )
+ return sorted(result)
+
+ def inlined_relations(self):
+ """return a list of (relation schema, target schemas, role) matching
+ given category(ies) and permission
+ """
+ return self._relations_by_section('inlined')
+
+ # inlined forms control ####################################################
+
+ def inlined_form_views(self):
+ """compute and return list of inlined form views (hosting the inlined
+ form object)
+ """
+ allformviews = []
+ entity = self.edited_entity
+ for rschema, ttypes, role in self.inlined_relations():
+ # show inline forms only if there's one possible target type
+ # for rschema
+ if len(ttypes) != 1:
+ self.warning('entity related by the %s relation should have '
+ 'inlined form but there is multiple target types, '
+ 'dunno what to do', rschema)
+ continue
+ tschema = ttypes[0]
+ ttype = tschema.type
+ formviews = list(self.inline_edition_form_view(rschema, ttype, role))
+ card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role)
+ # there is no related entity and we need at least one: we need to
+ # display one explicit inline-creation view
+ if self.should_display_inline_creation_form(rschema, formviews, card):
+ formviews += self.inline_creation_form_view(rschema, ttype, role)
+ # we can create more than one related entity, we thus display a link
+ # to add new related entities
+ if self.must_display_add_new_relation_link(rschema, role, tschema,
+ ttype, formviews, card):
+ addnewlink = self._cw.vreg['views'].select(
+ 'inline-addnew-link', self._cw,
+ etype=ttype, rtype=rschema, role=role, card=card,
+ peid=self.edited_entity.eid,
+ petype=self.edited_entity.e_schema, pform=self)
+ formviews.append(addnewlink)
+ allformviews += formviews
+ return allformviews
+
+ def should_display_inline_creation_form(self, rschema, existant, card):
+ """return true if a creation form should be inlined
+
+ by default true if there is no related entity and we need at least one
+ """
+ return not existant and card in '1+'
+
+ def should_display_add_new_relation_link(self, rschema, existant, card):
+ """return true if we should add a link to add a new creation form
+ (through ajax call)
+
+ by default true if there is no related entity or if the relation has
+ multiple cardinality
+ """
+ return not existant or card in '+*'
+
+ def must_display_add_new_relation_link(self, rschema, role, tschema,
+ ttype, existant, card):
+ """return true if we must add a link to add a new creation form
+ (through ajax call)
+
+ by default true if there is no related entity or if the relation has
+ multiple cardinality and it is permitted to add the inlined object and
+ relation.
+ """
+ return (self.should_display_add_new_relation_link(
+ rschema, existant, card) and
+ self.check_inlined_rdef_permissions(
+ rschema, role, tschema, ttype))
+
+ def check_inlined_rdef_permissions(self, rschema, role, tschema, ttype):
+ """return true if permissions are granted on the inlined object and
+ relation"""
+ if not tschema.has_perm(self._cw, 'add'):
+ return False
+ entity = self.edited_entity
+ rdef = entity.e_schema.rdef(rschema, role, ttype)
+ if entity.has_eid():
+ if role == 'subject':
+ rdefkwargs = {'fromeid': entity.eid}
+ else:
+ rdefkwargs = {'toeid': entity.eid}
+ return rdef.has_perm(self._cw, 'add', **rdefkwargs)
+ return rdef.may_have_permission('add', self._cw)
+
+
+ def should_hide_add_new_relation_link(self, rschema, card):
+ """return true if once an inlined creation form is added, the 'add new'
+ link should be hidden
+
+ by default true if the relation has single cardinality
+ """
+ return card in '1?'
+
+ def inline_edition_form_view(self, rschema, ttype, role):
+ """yield inline form views for already related entities through the
+ given relation
+ """
+ entity = self.edited_entity
+ related = entity.has_eid() and entity.related(rschema, role)
+ if related:
+ vvreg = self._cw.vreg['views']
+ # display inline-edition view for all existing related entities
+ for i, relentity in enumerate(related.entities()):
+ if relentity.cw_has_perm('update'):
+ yield vvreg.select('inline-edition', self._cw,
+ rset=related, row=i, col=0,
+ etype=ttype, rtype=rschema, role=role,
+ peid=entity.eid, pform=self)
+
+ def inline_creation_form_view(self, rschema, ttype, role):
+ """yield inline form views to a newly related (hence created) entity
+ through the given relation
+ """
+ try:
+ yield self._cw.vreg['views'].select('inline-creation', self._cw,
+ etype=ttype, rtype=rschema, role=role,
+ peid=self.edited_entity.eid,
+ petype=self.edited_entity.e_schema,
+ pform=self)
+ except NoSelectableObject:
+ # may be raised if user doesn't have the permission to add ttype entities (no checked
+ # earlier) or if there is some custom selector on the view
+ pass
+
+
+## default form ui configuration ##############################################
+
+_AFS = uicfg.autoform_section
+# use primary and not generated for eid since it has to be an hidden
+_AFS.tag_attribute(('*', 'eid'), 'main', 'hidden')
+_AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes')
+_AFS.tag_attribute(('*', 'description'), 'main', 'attributes')
+_AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'in_state', '*'), 'main', 'hidden')
+for rtype in ('creation_date', 'modification_date', 'cwuri',
+ 'owned_by', 'created_by', 'cw_source'):
+ _AFS.tag_subject_of(('*', rtype, '*'), 'main', 'metadata')
+
+_AFS.tag_subject_of(('*', 'by_transition', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('*', 'by_transition', '*'), 'muledit', 'attributes')
+_AFS.tag_object_of(('*', 'by_transition', '*'), 'main', 'hidden')
+_AFS.tag_object_of(('*', 'from_state', '*'), 'main', 'hidden')
+_AFS.tag_object_of(('*', 'to_state', '*'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'muledit', 'attributes')
+_AFS.tag_object_of(('*', 'wf_info_for', '*'), 'main', 'hidden')
+_AFS.tag_attribute(('CWEType', 'final'), 'main', 'hidden')
+_AFS.tag_attribute(('CWRType', 'final'), 'main', 'hidden')
+_AFS.tag_attribute(('CWUser', 'firstname'), 'main', 'attributes')
+_AFS.tag_attribute(('CWUser', 'surname'), 'main', 'attributes')
+_AFS.tag_attribute(('CWUser', 'last_login_time'), 'main', 'metadata')
+_AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'muledit', 'attributes')
+_AFS.tag_subject_of(('*', 'primary_email', '*'), 'main', 'relations')
+_AFS.tag_subject_of(('*', 'use_email', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'relation_type', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'from_entity', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'to_entity', '*'), 'main', 'inlined')
+
+_AFFK = uicfg.autoform_field_kwargs
+_AFFK.tag_attribute(('RQLExpression', 'expression'),
+ {'widget': fw.TextInput})
+_AFFK.tag_subject_of(('TrInfo', 'wf_info_for', '*'),
+ {'widget': fw.HiddenInput})
+
+def registration_callback(vreg):
+ global etype_relation_field
+
+ def etype_relation_field(etype, rtype, role='subject'):
+ try:
+ eschema = vreg.schema.eschema(etype)
+ return AutomaticEntityForm.field_by_name(rtype, role, eschema)
+ except (KeyError, f.FieldNotFound):
+ # catch KeyError raised when etype/rtype not found in schema
+ AutomaticEntityForm.error('field for %s %s may not be found in schema' % (rtype, role))
+ return None
+
+ vreg.register_all(globals().values(), __name__)