cubicweb/web/views/autoform.py
changeset 11057 0b59724cb3f2
parent 10932 cb217b2b3463
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """
       
    19 .. autodocstring:: cubicweb.web.views.autoform::AutomaticEntityForm
       
    20 
       
    21 Configuration through uicfg
       
    22 ```````````````````````````
       
    23 
       
    24 It is possible to manage which and how an entity's attributes and relations
       
    25 will be edited in the various contexts where the automatic entity form is used
       
    26 by using proper uicfg tags.
       
    27 
       
    28 The details of the uicfg syntax can be found in the :ref:`uicfg` chapter.
       
    29 
       
    30 Possible relation tags that apply to entity forms are detailled below.
       
    31 They are all in the :mod:`cubicweb.web.uicfg` module.
       
    32 
       
    33 Attributes/relations display location
       
    34 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       
    35 
       
    36 ``autoform_section`` specifies where to display a relation in form for a given
       
    37 form type.  :meth:`tag_attribute`, :meth:`tag_subject_of` and
       
    38 :meth:`tag_object_of` methods for this relation tag expect two arguments
       
    39 additionally to the relation key: a `formtype` and a `section`.
       
    40 
       
    41 `formtype` may be one of:
       
    42 
       
    43 * 'main', the main entity form (e.g. the one you get when creating or editing an
       
    44   entity)
       
    45 
       
    46 * 'inlined', the form for an entity inlined into another form
       
    47 
       
    48 * 'muledit', the table form when editing multiple entities of the same type
       
    49 
       
    50 
       
    51 section may be one of:
       
    52 
       
    53 * 'hidden', don't display (not even in a hidden input)
       
    54 
       
    55 * 'attributes', display in the attributes section
       
    56 
       
    57 * 'relations', display in the relations section, using the generic relation
       
    58   selector combobox (available in main form only, and not usable for attributes)
       
    59 
       
    60 * 'inlined', display target entity of the relation into an inlined form
       
    61   (available in main form only, and not for attributes)
       
    62 
       
    63 By default, mandatory relations are displayed in the 'attributes' section,
       
    64 others in 'relations' section.
       
    65 
       
    66 
       
    67 Change default fields
       
    68 ^^^^^^^^^^^^^^^^^^^^^
       
    69 
       
    70 Use ``autoform_field`` to replace the default field class to use for a relation
       
    71 or attribute. You can put either a field class or instance as value (put a class
       
    72 whenether it's possible).
       
    73 
       
    74 .. Warning::
       
    75 
       
    76    `autoform_field_kwargs` should usually be used instead of
       
    77    `autoform_field`. If you put a field instance into `autoform_field`,
       
    78    `autoform_field_kwargs` values for this relation will be ignored.
       
    79 
       
    80 
       
    81 Customize field options
       
    82 ^^^^^^^^^^^^^^^^^^^^^^^
       
    83 
       
    84 In order to customize field options (see :class:`~cubicweb.web.formfields.Field`
       
    85 for a detailed list of options), use `autoform_field_kwargs`. This rtag takes
       
    86 a dictionary as arguments, that will be given to the field's contructor.
       
    87 
       
    88 You can then put in that dictionary any arguments supported by the field
       
    89 class. For instance:
       
    90 
       
    91 .. sourcecode:: python
       
    92 
       
    93    # Change the content of the combobox. Here `ticket_done_in_choices` is a
       
    94    # function which returns a list of elements to populate the combobox
       
    95    autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'),
       
    96                                         {'sort': False,
       
    97                                          'choices': ticket_done_in_choices})
       
    98 
       
    99    # Force usage of a TextInput widget for the expression attribute of
       
   100    # RQLExpression entities
       
   101    autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'),
       
   102                                        {'widget': fw.TextInput})
       
   103 
       
   104 .. note::
       
   105 
       
   106    the widget argument can be either a class or an instance (the later
       
   107    case being convenient to pass the Widget specific initialisation
       
   108    options)
       
   109 
       
   110 Overriding permissions
       
   111 ^^^^^^^^^^^^^^^^^^^^^^
       
   112 
       
   113 The `autoform_permissions_overrides` rtag provides a way to by-pass security
       
   114 checking for dark-corner case where it can't be verified properly.
       
   115 
       
   116 
       
   117 .. More about inlined forms
       
   118 .. Controlling the generic relation fields
       
   119 """
       
   120 
       
   121 __docformat__ = "restructuredtext en"
       
   122 from cubicweb import _
       
   123 
       
   124 from warnings import warn
       
   125 
       
   126 from six.moves import range
       
   127 
       
   128 from logilab.mtconverter import xml_escape
       
   129 from logilab.common.decorators import iclassmethod, cached
       
   130 from logilab.common.deprecation import deprecated
       
   131 from logilab.common.registry import NoSelectableObject
       
   132 
       
   133 from cubicweb import neg_role, uilib
       
   134 from cubicweb.schema import display_name
       
   135 from cubicweb.view import EntityView
       
   136 from cubicweb.predicates import (
       
   137     match_kwargs, match_form_params, non_final_entity,
       
   138     specified_etype_implements)
       
   139 from cubicweb.utils import json_dumps
       
   140 from cubicweb.web import (stdmsgs, eid_param,
       
   141                           form as f, formwidgets as fw, formfields as ff)
       
   142 from cubicweb.web.views import uicfg, forms
       
   143 from cubicweb.web.views.ajaxcontroller import ajaxfunc
       
   144 
       
   145 
       
   146 # inlined form handling ########################################################
       
   147 
       
   148 class InlinedFormField(ff.Field):
       
   149     def __init__(self, view=None, **kwargs):
       
   150         kwargs.setdefault('label', None)
       
   151         # don't add eidparam=True since this field doesn't actually hold the
       
   152         # relation value (the subform does) hence should not be listed in
       
   153         # _cw_entity_fields
       
   154         super(InlinedFormField, self).__init__(name=view.rtype, role=view.role,
       
   155                                                **kwargs)
       
   156         self.view = view
       
   157 
       
   158     def render(self, form, renderer):
       
   159         """render this field, which is part of form, using the given form
       
   160         renderer
       
   161         """
       
   162         view = self.view
       
   163         i18nctx = 'inlined:%s.%s.%s' % (form.edited_entity.e_schema,
       
   164                                         view.rtype, view.role)
       
   165         return u'<div class="inline-%s-%s-slot">%s</div>' % (
       
   166             view.rtype, view.role,
       
   167             view.render(i18nctx=i18nctx, row=view.cw_row, col=view.cw_col))
       
   168 
       
   169     def form_init(self, form):
       
   170         """method called before by build_context to trigger potential field
       
   171         initialization requiring the form instance
       
   172         """
       
   173         if self.view.form:
       
   174             self.view.form.build_context(form.formvalues)
       
   175 
       
   176     @property
       
   177     def needs_multipart(self):
       
   178         if self.view.form:
       
   179             # take a look at inlined forms to check (recursively) if they need
       
   180             # multipart handling.
       
   181             return self.view.form.needs_multipart
       
   182         return False
       
   183 
       
   184     def has_been_modified(self, form):
       
   185         return False
       
   186 
       
   187     def process_posted(self, form):
       
   188         pass # handled by the subform
       
   189 
       
   190 
       
   191 class InlineEntityEditionFormView(f.FormViewMixIn, EntityView):
       
   192     """
       
   193     :attr peid: the parent entity's eid hosting the inline form
       
   194     :attr rtype: the relation bridging `etype` and `peid`
       
   195     :attr role: the role played by the `peid` in the relation
       
   196     :attr pform: the parent form where this inlined form is being displayed
       
   197     """
       
   198     __regid__ = 'inline-edition'
       
   199     __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
       
   200 
       
   201     _select_attrs = ('peid', 'rtype', 'role', 'pform', 'etype')
       
   202     removejs = "removeInlinedEntity('%s', '%s', '%s')"
       
   203 
       
   204     # make pylint happy
       
   205     peid = rtype = role = pform = etype = None
       
   206 
       
   207     def __init__(self, *args, **kwargs):
       
   208         for attr in self._select_attrs:
       
   209             # don't pop attributes from kwargs, so the end-up in
       
   210             # self.cw_extra_kwargs which is then passed to the edition form (see
       
   211             # the .form method)
       
   212             setattr(self, attr, kwargs.get(attr))
       
   213         super(InlineEntityEditionFormView, self).__init__(*args, **kwargs)
       
   214 
       
   215     def _entity(self):
       
   216         assert self.cw_row is not None, self
       
   217         return self.cw_rset.get_entity(self.cw_row, self.cw_col)
       
   218 
       
   219     @property
       
   220     def petype(self):
       
   221         assert isinstance(self.peid, int)
       
   222         pentity = self._cw.entity_from_eid(self.peid)
       
   223         return pentity.e_schema.type
       
   224 
       
   225     @property
       
   226     @cached
       
   227     def form(self):
       
   228         entity = self._entity()
       
   229         form = self._cw.vreg['forms'].select('edition', self._cw,
       
   230                                              entity=entity,
       
   231                                              formtype='inlined',
       
   232                                              form_renderer_id='inline',
       
   233                                              copy_nav_params=False,
       
   234                                              mainform=False,
       
   235                                              parent_form=self.pform,
       
   236                                              **self.cw_extra_kwargs)
       
   237         if self.pform is None:
       
   238             form.restore_previous_post(form.session_key())
       
   239         #assert form.parent_form
       
   240         self.add_hiddens(form, entity)
       
   241         return form
       
   242 
       
   243     def cell_call(self, row, col, i18nctx, **kwargs):
       
   244         """
       
   245         :param peid: the parent entity's eid hosting the inline form
       
   246         :param rtype: the relation bridging `etype` and `peid`
       
   247         :param role: the role played by the `peid` in the relation
       
   248         """
       
   249         entity = self._entity()
       
   250         divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (
       
   251             self.peid, self.rtype, entity.eid)
       
   252         self.render_form(i18nctx, divonclick=divonclick, **kwargs)
       
   253 
       
   254     def _get_removejs(self):
       
   255         """
       
   256         Don't display the remove link in edition form if the
       
   257         cardinality is 1. Handled in InlineEntityCreationFormView for
       
   258         creation form.
       
   259         """
       
   260         entity = self._entity()
       
   261         rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
       
   262         card = rdef.role_cardinality(self.role)
       
   263         if card == '1': # don't display remove link
       
   264             return None
       
   265         # if cardinality is 1..n (+), dont display link to remove an inlined form for the first form
       
   266         # allowing to edit the relation. To detect so:
       
   267         #
       
   268         # * if parent form (pform) is None, we're generated through an ajax call and so we know this
       
   269         #   is not the first form
       
   270         #
       
   271         # * if parent form is not None, look for previous InlinedFormField in the parent's form
       
   272         #   fields
       
   273         if card == '+' and self.pform is not None:
       
   274             # retrieve all field'views handling this relation and return None if we're the first of
       
   275             # them
       
   276             first_view = next(iter((f.view for f in self.pform.fields
       
   277                                     if isinstance(f, InlinedFormField)
       
   278                                     and f.view.rtype == self.rtype and f.view.role == self.role)))
       
   279             if self == first_view:
       
   280                 return None
       
   281         return self.removejs and self.removejs % (
       
   282             self.peid, self.rtype, entity.eid)
       
   283 
       
   284     def render_form(self, i18nctx, **kwargs):
       
   285         """fetch and render the form"""
       
   286         entity = self._entity()
       
   287         divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid)
       
   288         title = self.form_title(entity, i18nctx)
       
   289         removejs = self._get_removejs()
       
   290         countkey = '%s_count' % self.rtype
       
   291         try:
       
   292             self._cw.data[countkey] += 1
       
   293         except KeyError:
       
   294             self._cw.data[countkey] = 1
       
   295         self.form.render(w=self.w, divid=divid, title=title, removejs=removejs,
       
   296                          i18nctx=i18nctx, counter=self._cw.data[countkey] ,
       
   297                          **kwargs)
       
   298 
       
   299     def form_title(self, entity, i18nctx):
       
   300         return self._cw.pgettext(i18nctx, entity.cw_etype)
       
   301 
       
   302     def add_hiddens(self, form, entity):
       
   303         """to ease overriding (see cubes.vcsfile.views.forms for instance)"""
       
   304         iid = 'rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid)
       
   305         #  * str(self.rtype) in case it's a schema object
       
   306         #  * neged_role() since role is the for parent entity, we want the role
       
   307         #    of the inlined entity
       
   308         form.add_hidden(name=str(self.rtype), value=self.peid,
       
   309                         role=neg_role(self.role), eidparam=True, id=iid)
       
   310 
       
   311     def keep_entity(self, form, entity):
       
   312         if not entity.has_eid():
       
   313             return True
       
   314         # are we regenerating form because of a validation error?
       
   315         if form.form_previous_values:
       
   316             cdvalues = self._cw.list_form_param(eid_param(self.rtype, self.peid),
       
   317                                                 form.form_previous_values)
       
   318             if unicode(entity.eid) not in cdvalues:
       
   319                 return False
       
   320         return True
       
   321 
       
   322 
       
   323 class InlineEntityCreationFormView(InlineEntityEditionFormView):
       
   324     """
       
   325     :attr etype: the entity type being created in the inline form
       
   326     """
       
   327     __regid__ = 'inline-creation'
       
   328     __select__ = (match_kwargs('peid', 'petype', 'rtype')
       
   329                   & specified_etype_implements('Any'))
       
   330     _select_attrs = InlineEntityEditionFormView._select_attrs + ('petype',)
       
   331 
       
   332     # make pylint happy
       
   333     petype = None
       
   334 
       
   335     @property
       
   336     def removejs(self):
       
   337         entity = self._entity()
       
   338         rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
       
   339         card = rdef.role_cardinality(self.role)
       
   340         # when one is adding an inline entity for a relation of a single card,
       
   341         # the 'add a new xxx' link disappears. If the user then cancel the addition,
       
   342         # we have to make this link appears back. This is done by giving add new link
       
   343         # id to removeInlineForm.
       
   344         if card == '?':
       
   345             divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
       
   346             return "removeInlineForm('%%s', '%%s', '%s', '%%s', '%s')" % (
       
   347                 self.role, divid)
       
   348         elif card in '+*':
       
   349             return "removeInlineForm('%%s', '%%s', '%s', '%%s')" % self.role
       
   350         # don't do anything for card == '1'
       
   351 
       
   352     @cached
       
   353     def _entity(self):
       
   354         try:
       
   355             cls = self._cw.vreg['etypes'].etype_class(self.etype)
       
   356         except Exception:
       
   357             self.w(self._cw._('no such entity type %s') % self.etype)
       
   358             return
       
   359         entity = cls(self._cw)
       
   360         entity.eid = next(self._cw.varmaker)
       
   361         return entity
       
   362 
       
   363     def call(self, i18nctx, **kwargs):
       
   364         self.render_form(i18nctx, **kwargs)
       
   365 
       
   366 
       
   367 class InlineAddNewLinkView(InlineEntityCreationFormView):
       
   368     """
       
   369     :attr card: the cardinality of the relation according to role of `peid`
       
   370     """
       
   371     __regid__ = 'inline-addnew-link'
       
   372     __select__ = (match_kwargs('peid', 'petype', 'rtype')
       
   373                   & specified_etype_implements('Any'))
       
   374 
       
   375     _select_attrs = InlineEntityCreationFormView._select_attrs + ('card',)
       
   376     card = None # make pylint happy
       
   377     form = None # no actual form wrapped
       
   378 
       
   379     def call(self, i18nctx, **kwargs):
       
   380         self._cw.set_varmaker()
       
   381         divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
       
   382         self.w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
       
   383           % divid)
       
   384         js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s', '%s')" % (
       
   385             self.peid, self.petype, self.etype, self.rtype, self.role, i18nctx)
       
   386         if self.pform.should_hide_add_new_relation_link(self.rtype, self.card):
       
   387             js = "toggleVisibility('%s'); %s" % (divid, js)
       
   388         __ = self._cw.pgettext
       
   389         self.w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
       
   390           % (self.rtype, self.peid, js, __(i18nctx, 'add a %s' % self.etype)))
       
   391         self.w(u'</div>')
       
   392 
       
   393 
       
   394 # generic relations handling ##################################################
       
   395 
       
   396 def relation_id(eid, rtype, role, reid):
       
   397     """return an identifier for a relation between two entities"""
       
   398     if role == 'subject':
       
   399         return u'%s:%s:%s' % (eid, rtype, reid)
       
   400     return u'%s:%s:%s' % (reid, rtype, eid)
       
   401 
       
   402 def toggleable_relation_link(eid, nodeid, label='x'):
       
   403     """return javascript snippet to delete/undelete a relation between two
       
   404     entities
       
   405     """
       
   406     js = u"javascript: togglePendingDelete('%s', %s);" % (
       
   407         nodeid, xml_escape(json_dumps(eid)))
       
   408     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
       
   409         js, nodeid, label)
       
   410 
       
   411 
       
   412 def get_pending_inserts(req, eid=None):
       
   413     """shortcut to access req's pending_insert entry
       
   414 
       
   415     This is where are stored relations being added while editing
       
   416     an entity. This used to be stored in a temporary cookie.
       
   417     """
       
   418     pending = req.session.data.get('pending_insert', ())
       
   419     return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
       
   420             if eid is None or eid in (subj, obj)]
       
   421 
       
   422 def get_pending_deletes(req, eid=None):
       
   423     """shortcut to access req's pending_delete entry
       
   424 
       
   425     This is where are stored relations being removed while editing
       
   426     an entity. This used to be stored in a temporary cookie.
       
   427     """
       
   428     pending = req.session.data.get('pending_delete', ())
       
   429     return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
       
   430             if eid is None or eid in (subj, obj)]
       
   431 
       
   432 def parse_relations_descr(rdescr):
       
   433     """parse a string describing some relations, in the form
       
   434     subjeids:rtype:objeids
       
   435     where subjeids and objeids are eids separeted by a underscore
       
   436 
       
   437     return an iterator on (subject eid, relation type, object eid) found
       
   438     """
       
   439     for rstr in rdescr:
       
   440         subjs, rtype, objs = rstr.split(':')
       
   441         for subj in subjs.split('_'):
       
   442             for obj in objs.split('_'):
       
   443                 yield int(subj), rtype, int(obj)
       
   444 
       
   445 def delete_relations(req, rdefs):
       
   446     """delete relations from the repository"""
       
   447     # FIXME convert to using the syntax subject:relation:eids
       
   448     execute = req.execute
       
   449     for subj, rtype, obj in parse_relations_descr(rdefs):
       
   450         rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
       
   451         execute(rql, {'x': subj, 'y': obj})
       
   452     req.set_message(req._('relations deleted'))
       
   453 
       
   454 def insert_relations(req, rdefs):
       
   455     """insert relations into the repository"""
       
   456     execute = req.execute
       
   457     for subj, rtype, obj in parse_relations_descr(rdefs):
       
   458         rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
       
   459         execute(rql, {'x': subj, 'y': obj})
       
   460 
       
   461 
       
   462 # ajax edition helpers ########################################################
       
   463 @ajaxfunc(output_type='xhtml', check_pageid=True)
       
   464 def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
       
   465     view = self._cw.vreg['views'].select('inline-creation', self._cw,
       
   466                                          etype=ttype, rtype=rtype, role=role,
       
   467                                          peid=peid, petype=petype)
       
   468     return self._call_view(view, i18nctx=i18nctx)
       
   469 
       
   470 @ajaxfunc(output_type='json')
       
   471 def validate_form(self, action, names, values):
       
   472     return self.validate_form(action, names, values)
       
   473 
       
   474 @ajaxfunc
       
   475 def cancel_edition(self, errorurl):
       
   476     """cancelling edition from javascript
       
   477 
       
   478     We need to clear associated req's data :
       
   479       - errorurl
       
   480       - pending insertions / deletions
       
   481     """
       
   482     self._cw.cancel_edition(errorurl)
       
   483 
       
   484 
       
   485 def _add_pending(req, eidfrom, rel, eidto, kind):
       
   486     key = 'pending_%s' % kind
       
   487     pendings = req.session.data.setdefault(key, set())
       
   488     pendings.add( (int(eidfrom), rel, int(eidto)) )
       
   489 
       
   490 def _remove_pending(req, eidfrom, rel, eidto, kind):
       
   491     key = 'pending_%s' % kind
       
   492     pendings = req.session.data[key]
       
   493     pendings.remove( (int(eidfrom), rel, int(eidto)) )
       
   494 
       
   495 @ajaxfunc(output_type='json')
       
   496 def remove_pending_insert(self, args):
       
   497     eidfrom, rel, eidto = args
       
   498     _remove_pending(self._cw, eidfrom, rel, eidto, 'insert')
       
   499 
       
   500 @ajaxfunc(output_type='json')
       
   501 def add_pending_inserts(self, tripletlist):
       
   502     for eidfrom, rel, eidto in tripletlist:
       
   503         _add_pending(self._cw, eidfrom, rel, eidto, 'insert')
       
   504 
       
   505 @ajaxfunc(output_type='json')
       
   506 def remove_pending_delete(self, args):
       
   507     eidfrom, rel, eidto = args
       
   508     _remove_pending(self._cw, eidfrom, rel, eidto, 'delete')
       
   509 
       
   510 @ajaxfunc(output_type='json')
       
   511 def add_pending_delete(self, args):
       
   512     eidfrom, rel, eidto = args
       
   513     _add_pending(self._cw, eidfrom, rel, eidto, 'delete')
       
   514 
       
   515 
       
   516 class GenericRelationsWidget(fw.FieldWidget):
       
   517 
       
   518     def render(self, form, field, renderer):
       
   519         stream = []
       
   520         w = stream.append
       
   521         req = form._cw
       
   522         _ = req._
       
   523         __ = _
       
   524         eid = form.edited_entity.eid
       
   525         w(u'<table id="relatedEntities">')
       
   526         for rschema, role, related in field.relations_table(form):
       
   527             # already linked entities
       
   528             if related:
       
   529                 label = rschema.display_name(req, role, context=form.edited_entity.cw_etype)
       
   530                 w(u'<tr><th class="labelCol">%s</th>' % label)
       
   531                 w(u'<td>')
       
   532                 w(u'<ul class="list-unstyled">')
       
   533                 for viewparams in related:
       
   534                     w(u'<li>%s<span id="span%s" class="%s">%s</span></li>'
       
   535                       % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
       
   536                 if not form.force_display and form.maxrelitems < len(related):
       
   537                     link = (u'<span>'
       
   538                             '[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
       
   539                             '</span>' % _('view all'))
       
   540                     w(u'<li>%s</li>' % link)
       
   541                 w(u'</ul>')
       
   542                 w(u'</td>')
       
   543                 w(u'</tr>')
       
   544         pendings = list(field.restore_pending_inserts(form))
       
   545         if not pendings:
       
   546             w(u'<tr><th>&#160;</th><td>&#160;</td></tr>')
       
   547         else:
       
   548             for row in pendings:
       
   549                 # soon to be linked to entities
       
   550                 w(u'<tr id="tr%s">' % row[1])
       
   551                 w(u'<th>%s</th>' % row[3])
       
   552                 w(u'<td>')
       
   553                 w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
       
   554                   (_('cancel this insert'), row[2]))
       
   555                 w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
       
   556                   % (row[1], row[4], xml_escape(row[5])))
       
   557                 w(u'</td>')
       
   558                 w(u'</tr>')
       
   559         w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
       
   560         w(u'<th class="labelCol">')
       
   561         w(u'<select id="relationSelector_%s" tabindex="%s" '
       
   562           'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
       
   563           % (eid, req.next_tabindex(), xml_escape(json_dumps(eid))))
       
   564         w(u'<option value="">%s</option>' % _('select a relation'))
       
   565         for i18nrtype, rschema, role in field.relations:
       
   566             # more entities to link to
       
   567             w(u'<option value="%s_%s">%s</option>' % (rschema, role, i18nrtype))
       
   568         w(u'</select>')
       
   569         w(u'</th>')
       
   570         w(u'<td id="unrelatedDivs_%s"></td>' % eid)
       
   571         w(u'</tr>')
       
   572         w(u'</table>')
       
   573         return '\n'.join(stream)
       
   574 
       
   575 
       
   576 class GenericRelationsField(ff.Field):
       
   577     widget = GenericRelationsWidget
       
   578 
       
   579     def __init__(self, relations, name='_cw_generic_field', **kwargs):
       
   580         assert relations
       
   581         kwargs['eidparam'] = True
       
   582         super(GenericRelationsField, self).__init__(name, **kwargs)
       
   583         self.relations = relations
       
   584 
       
   585     def process_posted(self, form):
       
   586         todelete = get_pending_deletes(form._cw)
       
   587         if todelete:
       
   588             delete_relations(form._cw, todelete)
       
   589         toinsert = get_pending_inserts(form._cw)
       
   590         if toinsert:
       
   591             insert_relations(form._cw, toinsert)
       
   592         return ()
       
   593 
       
   594     def relations_table(self, form):
       
   595         """yiels 3-tuples (rtype, role, related_list)
       
   596         where <related_list> itself a list of :
       
   597           - node_id (will be the entity element's DOM id)
       
   598           - appropriate javascript's togglePendingDelete() function call
       
   599           - status 'pendingdelete' or ''
       
   600           - oneline view of related entity
       
   601         """
       
   602         entity = form.edited_entity
       
   603         pending_deletes = get_pending_deletes(form._cw, entity.eid)
       
   604         for label, rschema, role in self.relations:
       
   605             related = []
       
   606             if entity.has_eid():
       
   607                 rset = entity.related(rschema, role, limit=form.related_limit)
       
   608                 if role == 'subject':
       
   609                     haspermkwargs = {'fromeid': entity.eid}
       
   610                 else:
       
   611                     haspermkwargs = {'toeid': entity.eid}
       
   612                 if rschema.has_perm(form._cw, 'delete', **haspermkwargs):
       
   613                     toggleable_rel_link_func = toggleable_relation_link
       
   614                 else:
       
   615                     toggleable_rel_link_func = lambda x, y, z: u''
       
   616                 for row in range(rset.rowcount):
       
   617                     nodeid = relation_id(entity.eid, rschema, role,
       
   618                                          rset[row][0])
       
   619                     if nodeid in pending_deletes:
       
   620                         status, label = u'pendingDelete', '+'
       
   621                     else:
       
   622                         status, label = u'', 'x'
       
   623                     dellink = toggleable_rel_link_func(entity.eid, nodeid, label)
       
   624                     eview = form._cw.view('oneline', rset, row=row)
       
   625                     related.append((nodeid, dellink, status, eview))
       
   626             yield (rschema, role, related)
       
   627 
       
   628     def restore_pending_inserts(self, form):
       
   629         """used to restore edition page as it was before clicking on
       
   630         'search for <some entity type>'
       
   631         """
       
   632         entity = form.edited_entity
       
   633         pending_inserts = set(get_pending_inserts(form._cw, form.edited_entity.eid))
       
   634         for pendingid in pending_inserts:
       
   635             eidfrom, rtype, eidto = pendingid.split(':')
       
   636             pendingid = 'id' + pendingid
       
   637             if int(eidfrom) == entity.eid: # subject
       
   638                 label = display_name(form._cw, rtype, 'subject',
       
   639                                      entity.cw_etype)
       
   640                 reid = eidto
       
   641             else:
       
   642                 label = display_name(form._cw, rtype, 'object',
       
   643                                      entity.cw_etype)
       
   644                 reid = eidfrom
       
   645             jscall = "javascript: cancelPendingInsert('%s', 'tr', null, %s);" \
       
   646                      % (pendingid, entity.eid)
       
   647             rset = form._cw.eid_rset(reid)
       
   648             eview = form._cw.view('text', rset, row=0)
       
   649             yield rtype, pendingid, jscall, label, reid, eview
       
   650 
       
   651 
       
   652 class UnrelatedDivs(EntityView):
       
   653     __regid__ = 'unrelateddivs'
       
   654     __select__ = match_form_params('relation')
       
   655 
       
   656     def cell_call(self, row, col):
       
   657         entity = self.cw_rset.get_entity(row, col)
       
   658         relname, role = self._cw.form.get('relation').rsplit('_', 1)
       
   659         rschema = self._cw.vreg.schema.rschema(relname)
       
   660         hidden = 'hidden' in self._cw.form
       
   661         is_cell = 'is_cell' in self._cw.form
       
   662         self.w(self.build_unrelated_select_div(entity, rschema, role,
       
   663                                                is_cell=is_cell, hidden=hidden))
       
   664 
       
   665     def build_unrelated_select_div(self, entity, rschema, role,
       
   666                                    is_cell=False, hidden=True):
       
   667         options = []
       
   668         divid = 'div%s_%s_%s' % (rschema.type, role, entity.eid)
       
   669         selectid = 'select%s_%s_%s' % (rschema.type, role, entity.eid)
       
   670         if rschema.symmetric or role == 'subject':
       
   671             targettypes = rschema.objects(entity.e_schema)
       
   672             etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes))
       
   673         else:
       
   674             targettypes = rschema.subjects(entity.e_schema)
       
   675             etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes))
       
   676         etypes = uilib.cut(etypes, self._cw.property_value('navigation.short-line-size'))
       
   677         options.append('<option>%s %s</option>' % (self._cw._('select a'), etypes))
       
   678         options += self._get_select_options(entity, rschema, role)
       
   679         options += self._get_search_options(entity, rschema, role, targettypes)
       
   680         relname, role = self._cw.form.get('relation').rsplit('_', 1)
       
   681         return u"""\
       
   682 <div class="%s" id="%s">
       
   683   <select id="%s" onchange="javascript: addPendingInsert(this.options[this.selectedIndex], %s, %s, '%s');">
       
   684     %s
       
   685   </select>
       
   686 </div>
       
   687 """ % (hidden and 'hidden' or '', divid, selectid,
       
   688        xml_escape(json_dumps(entity.eid)), is_cell and 'true' or 'null', relname,
       
   689        '\n'.join(options))
       
   690 
       
   691     def _get_select_options(self, entity, rschema, role):
       
   692         """add options to search among all entities of each possible type"""
       
   693         options = []
       
   694         pending_inserts = get_pending_inserts(self._cw, entity.eid)
       
   695         rtype = rschema.type
       
   696         form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
       
   697         field = form.field_by_name(rschema, role, entity.e_schema)
       
   698         limit = self._cw.property_value('navigation.combobox-limit')
       
   699         # NOTE: expect 'limit' arg on choices method of relation field
       
   700         for eview, reid in field.vocabulary(form, limit=limit):
       
   701             if reid is None:
       
   702                 if eview: # skip blank value
       
   703                     options.append('<option class="separator">-- %s --</option>'
       
   704                                    % xml_escape(eview))
       
   705             elif reid != ff.INTERNAL_FIELD_VALUE:
       
   706                 optionid = relation_id(entity.eid, rtype, role, reid)
       
   707                 if optionid not in pending_inserts:
       
   708                     # prefix option's id with letters to make valid XHTML wise
       
   709                     options.append('<option id="id%s" value="%s">%s</option>' %
       
   710                                    (optionid, reid, xml_escape(eview)))
       
   711         return options
       
   712 
       
   713     def _get_search_options(self, entity, rschema, role, targettypes):
       
   714         """add options to search among all entities of each possible type"""
       
   715         options = []
       
   716         _ = self._cw._
       
   717         for eschema in targettypes:
       
   718             mode = '%s:%s:%s:%s' % (role, entity.eid, rschema.type, eschema)
       
   719             url = self._cw.build_url(entity.rest_path(), vid='search-associate',
       
   720                                  __mode=mode)
       
   721             options.append((eschema.display_name(self._cw),
       
   722                             '<option value="%s">%s %s</option>' % (
       
   723                 xml_escape(url), _('Search for'), eschema.display_name(self._cw))))
       
   724         return [o for l, o in sorted(options)]
       
   725 
       
   726 
       
   727 # The automatic entity form ####################################################
       
   728 
       
   729 class AutomaticEntityForm(forms.EntityFieldsForm):
       
   730     """AutomaticEntityForm is an automagic form to edit any entity. It
       
   731     is designed to be fully generated from schema but highly
       
   732     configurable through uicfg.
       
   733 
       
   734     Of course, as for other forms, you can also customise it by specifying
       
   735     various standard form parameters on selection, overriding, or
       
   736     adding/removing fields in selected instances.
       
   737     """
       
   738     __regid__ = 'edition'
       
   739 
       
   740     cwtarget = 'eformframe'
       
   741     cssclass = 'entityForm'
       
   742     copy_nav_params = True
       
   743     form_buttons = [fw.SubmitButton(),
       
   744                     fw.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'),
       
   745                     fw.Button(stdmsgs.BUTTON_CANCEL,
       
   746                               {'class': fw.Button.css_class + ' cwjs-edition-cancel'})]
       
   747     # for attributes selection when searching in uicfg.autoform_section
       
   748     formtype = 'main'
       
   749     # set this to a list of [(relation, role)] if you want to explictily tell
       
   750     # which relations should be edited
       
   751     display_fields = None
       
   752     # action on the form tag
       
   753     _default_form_action_path = 'validateform'
       
   754 
       
   755     @iclassmethod
       
   756     def field_by_name(cls_or_self, name, role=None, eschema=None):
       
   757         """return field with the given name and role. If field is not explicitly
       
   758         defined for the form but `eclass` is specified, guess_field will be
       
   759         called.
       
   760         """
       
   761         try:
       
   762             return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role, eschema)
       
   763         except f.FieldNotFound:
       
   764             if name == '_cw_generic_field' and not isinstance(cls_or_self, type):
       
   765                 return cls_or_self._generic_relations_field()
       
   766             raise
       
   767 
       
   768     # base automatic entity form methods #######################################
       
   769 
       
   770     def __init__(self, *args, **kwargs):
       
   771         super(AutomaticEntityForm, self).__init__(*args, **kwargs)
       
   772         self.uicfg_afs = self._cw.vreg['uicfg'].select(
       
   773             'autoform_section', self._cw, entity=self.edited_entity)
       
   774         entity = self.edited_entity
       
   775         if entity.has_eid():
       
   776             entity.complete()
       
   777         for rtype, role in self.editable_attributes():
       
   778             try:
       
   779                 self.field_by_name(str(rtype), role)
       
   780                 continue # explicitly specified
       
   781             except f.FieldNotFound:
       
   782                 # has to be guessed
       
   783                 try:
       
   784                     field = self.field_by_name(str(rtype), role,
       
   785                                                eschema=entity.e_schema)
       
   786                     self.fields.append(field)
       
   787                 except f.FieldNotFound:
       
   788                     # meta attribute such as <attr>_format
       
   789                     continue
       
   790         if self.fieldsets_in_order:
       
   791             fsio = list(self.fieldsets_in_order)
       
   792         else:
       
   793             fsio = [None]
       
   794         self.fieldsets_in_order = fsio
       
   795         # add fields for relation whose target should have an inline form
       
   796         for formview in self.inlined_form_views():
       
   797             field = self._inlined_form_view_field(formview)
       
   798             self.fields.append(field)
       
   799             if not field.fieldset in fsio:
       
   800                 fsio.append(field.fieldset)
       
   801         if self.formtype == 'main':
       
   802             # add the generic relation field if necessary
       
   803             if entity.has_eid() and (
       
   804                 self.display_fields is None or
       
   805                 '_cw_generic_field' in self.display_fields):
       
   806                 try:
       
   807                     field = self.field_by_name('_cw_generic_field')
       
   808                 except f.FieldNotFound:
       
   809                     # no editable relation
       
   810                     pass
       
   811                 else:
       
   812                     self.fields.append(field)
       
   813                     if not field.fieldset in fsio:
       
   814                         fsio.append(field.fieldset)
       
   815         self.maxrelitems = self._cw.property_value('navigation.related-limit')
       
   816         self.force_display = bool(self._cw.form.get('__force_display'))
       
   817         fnum = len(self.fields)
       
   818         self.fields.sort(key=lambda f: f.order is None and fnum or f.order)
       
   819 
       
   820     @property
       
   821     def related_limit(self):
       
   822         if self.force_display:
       
   823             return None
       
   824         return self.maxrelitems + 1
       
   825 
       
   826     # autoform specific fields #################################################
       
   827 
       
   828     def _generic_relations_field(self):
       
   829         srels_by_cat = self.editable_relations()
       
   830         if not srels_by_cat:
       
   831             raise f.FieldNotFound('_cw_generic_field')
       
   832         fieldset = 'This %s:' % self.edited_entity.e_schema
       
   833         return GenericRelationsField(self.editable_relations(),
       
   834                                      fieldset=fieldset, label=None)
       
   835 
       
   836     def _inlined_form_view_field(self, view):
       
   837         # XXX allow more customization
       
   838         kwargs = self.uicfg_affk.etype_get(self.edited_entity.e_schema,
       
   839                                            view.rtype, view.role, view.etype)
       
   840         if kwargs is None:
       
   841             kwargs = {}
       
   842         return InlinedFormField(view=view, **kwargs)
       
   843 
       
   844     # methods mapping edited entity relations to fields in the form ############
       
   845 
       
   846     def _relations_by_section(self, section, permission='add', strict=False):
       
   847         """return a list of (relation schema, target schemas, role) matching
       
   848         given category(ies) and permission
       
   849         """
       
   850         return self.uicfg_afs.relations_by_section(
       
   851             self.edited_entity, self.formtype, section, permission, strict)
       
   852 
       
   853     def editable_attributes(self, strict=False):
       
   854         """return a list of (relation schema, role) to edit for the entity"""
       
   855         if self.display_fields is not None:
       
   856             schema = self._cw.vreg.schema
       
   857             return [(schema[rtype], role) for rtype, role in self.display_fields]
       
   858         if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
       
   859             return []
       
   860         action = 'update' if self.edited_entity.has_eid() else 'add'
       
   861         return [(rtype, role) for rtype, _, role in self._relations_by_section(
       
   862             'attributes', action, strict)]
       
   863 
       
   864     def editable_relations(self):
       
   865         """return a sorted list of (relation's label, relation'schema, role) for
       
   866         relations in the 'relations' section
       
   867         """
       
   868         result = []
       
   869         for rschema, _, role in self._relations_by_section('relations',
       
   870                                                            strict=True):
       
   871             result.append( (rschema.display_name(self.edited_entity._cw, role,
       
   872                                                  self.edited_entity.cw_etype),
       
   873                             rschema, role) )
       
   874         return sorted(result)
       
   875 
       
   876     def inlined_relations(self):
       
   877         """return a list of (relation schema, target schemas, role) matching
       
   878         given category(ies) and permission
       
   879         """
       
   880         return self._relations_by_section('inlined')
       
   881 
       
   882     # inlined forms control ####################################################
       
   883 
       
   884     def inlined_form_views(self):
       
   885         """compute and return list of inlined form views (hosting the inlined
       
   886         form object)
       
   887         """
       
   888         allformviews = []
       
   889         entity = self.edited_entity
       
   890         for rschema, ttypes, role in self.inlined_relations():
       
   891             # show inline forms only if there's one possible target type
       
   892             # for rschema
       
   893             if len(ttypes) != 1:
       
   894                 self.warning('entity related by the %s relation should have '
       
   895                              'inlined form but there is multiple target types, '
       
   896                              'dunno what to do', rschema)
       
   897                 continue
       
   898             tschema = ttypes[0]
       
   899             ttype = tschema.type
       
   900             formviews = list(self.inline_edition_form_view(rschema, ttype, role))
       
   901             card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role)
       
   902             # there is no related entity and we need at least one: we need to
       
   903             # display one explicit inline-creation view
       
   904             if self.should_display_inline_creation_form(rschema, formviews, card):
       
   905                 formviews += self.inline_creation_form_view(rschema, ttype, role)
       
   906             # we can create more than one related entity, we thus display a link
       
   907             # to add new related entities
       
   908             if self.must_display_add_new_relation_link(rschema, role, tschema,
       
   909                                                        ttype, formviews, card):
       
   910                 addnewlink = self._cw.vreg['views'].select(
       
   911                     'inline-addnew-link', self._cw,
       
   912                     etype=ttype, rtype=rschema, role=role, card=card,
       
   913                     peid=self.edited_entity.eid,
       
   914                     petype=self.edited_entity.e_schema, pform=self)
       
   915                 formviews.append(addnewlink)
       
   916             allformviews += formviews
       
   917         return allformviews
       
   918 
       
   919     def should_display_inline_creation_form(self, rschema, existant, card):
       
   920         """return true if a creation form should be inlined
       
   921 
       
   922         by default true if there is no related entity and we need at least one
       
   923         """
       
   924         return not existant and card in '1+'
       
   925 
       
   926     def should_display_add_new_relation_link(self, rschema, existant, card):
       
   927         """return true if we should add a link to add a new creation form
       
   928         (through ajax call)
       
   929 
       
   930         by default true if there is no related entity or if the relation has
       
   931         multiple cardinality
       
   932         """
       
   933         return not existant or card in '+*'
       
   934 
       
   935     def must_display_add_new_relation_link(self, rschema, role, tschema,
       
   936                                            ttype, existant, card):
       
   937         """return true if we must add a link to add a new creation form
       
   938         (through ajax call)
       
   939 
       
   940         by default true if there is no related entity or if the relation has
       
   941         multiple cardinality and it is permitted to add the inlined object and
       
   942         relation.
       
   943         """
       
   944         return (self.should_display_add_new_relation_link(
       
   945                     rschema, existant, card) and
       
   946                 self.check_inlined_rdef_permissions(
       
   947                     rschema, role, tschema, ttype))
       
   948 
       
   949     def check_inlined_rdef_permissions(self, rschema, role, tschema, ttype):
       
   950         """return true if permissions are granted on the inlined object and
       
   951         relation"""
       
   952         if not tschema.has_perm(self._cw, 'add'):
       
   953             return False
       
   954         entity = self.edited_entity
       
   955         rdef = entity.e_schema.rdef(rschema, role, ttype)
       
   956         if entity.has_eid():
       
   957             if role == 'subject':
       
   958                 rdefkwargs = {'fromeid': entity.eid}
       
   959             else:
       
   960                 rdefkwargs = {'toeid': entity.eid}
       
   961             return rdef.has_perm(self._cw, 'add', **rdefkwargs)
       
   962         return rdef.may_have_permission('add', self._cw)
       
   963 
       
   964 
       
   965     def should_hide_add_new_relation_link(self, rschema, card):
       
   966         """return true if once an inlined creation form is added, the 'add new'
       
   967         link should be hidden
       
   968 
       
   969         by default true if the relation has single cardinality
       
   970         """
       
   971         return card in '1?'
       
   972 
       
   973     def inline_edition_form_view(self, rschema, ttype, role):
       
   974         """yield inline form views for already related entities through the
       
   975         given relation
       
   976         """
       
   977         entity = self.edited_entity
       
   978         related = entity.has_eid() and entity.related(rschema, role)
       
   979         if related:
       
   980             vvreg = self._cw.vreg['views']
       
   981             # display inline-edition view for all existing related entities
       
   982             for i, relentity in enumerate(related.entities()):
       
   983                 if relentity.cw_has_perm('update'):
       
   984                     yield vvreg.select('inline-edition', self._cw,
       
   985                                        rset=related, row=i, col=0,
       
   986                                        etype=ttype, rtype=rschema, role=role,
       
   987                                        peid=entity.eid, pform=self)
       
   988 
       
   989     def inline_creation_form_view(self, rschema, ttype, role):
       
   990         """yield inline form views to a newly related (hence created) entity
       
   991         through the given relation
       
   992         """
       
   993         try:
       
   994             yield self._cw.vreg['views'].select('inline-creation', self._cw,
       
   995                                                 etype=ttype, rtype=rschema, role=role,
       
   996                                                 peid=self.edited_entity.eid,
       
   997                                                 petype=self.edited_entity.e_schema,
       
   998                                                 pform=self)
       
   999         except NoSelectableObject:
       
  1000             # may be raised if user doesn't have the permission to add ttype entities (no checked
       
  1001             # earlier) or if there is some custom selector on the view
       
  1002             pass
       
  1003 
       
  1004 
       
  1005 ## default form ui configuration ##############################################
       
  1006 
       
  1007 _AFS = uicfg.autoform_section
       
  1008 # use primary and not generated for eid since it has to be an hidden
       
  1009 _AFS.tag_attribute(('*', 'eid'), 'main', 'hidden')
       
  1010 _AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes')
       
  1011 _AFS.tag_attribute(('*', 'description'), 'main', 'attributes')
       
  1012 _AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden')
       
  1013 _AFS.tag_subject_of(('*', 'in_state', '*'), 'main', 'hidden')
       
  1014 for rtype in ('creation_date', 'modification_date', 'cwuri',
       
  1015               'owned_by', 'created_by', 'cw_source'):
       
  1016     _AFS.tag_subject_of(('*', rtype, '*'), 'main', 'metadata')
       
  1017 
       
  1018 _AFS.tag_subject_of(('*', 'by_transition', '*'), 'main', 'attributes')
       
  1019 _AFS.tag_subject_of(('*', 'by_transition', '*'), 'muledit', 'attributes')
       
  1020 _AFS.tag_object_of(('*', 'by_transition', '*'), 'main', 'hidden')
       
  1021 _AFS.tag_object_of(('*', 'from_state', '*'), 'main', 'hidden')
       
  1022 _AFS.tag_object_of(('*', 'to_state', '*'), 'main', 'hidden')
       
  1023 _AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'main', 'attributes')
       
  1024 _AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'muledit', 'attributes')
       
  1025 _AFS.tag_object_of(('*', 'wf_info_for', '*'), 'main', 'hidden')
       
  1026 _AFS.tag_attribute(('CWEType', 'final'), 'main', 'hidden')
       
  1027 _AFS.tag_attribute(('CWRType', 'final'), 'main', 'hidden')
       
  1028 _AFS.tag_attribute(('CWUser', 'firstname'), 'main', 'attributes')
       
  1029 _AFS.tag_attribute(('CWUser', 'surname'), 'main', 'attributes')
       
  1030 _AFS.tag_attribute(('CWUser', 'last_login_time'), 'main', 'metadata')
       
  1031 _AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'main', 'attributes')
       
  1032 _AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'muledit', 'attributes')
       
  1033 _AFS.tag_subject_of(('*', 'primary_email', '*'), 'main', 'relations')
       
  1034 _AFS.tag_subject_of(('*', 'use_email', '*'), 'main', 'inlined')
       
  1035 _AFS.tag_subject_of(('CWRelation', 'relation_type', '*'), 'main', 'inlined')
       
  1036 _AFS.tag_subject_of(('CWRelation', 'from_entity', '*'), 'main', 'inlined')
       
  1037 _AFS.tag_subject_of(('CWRelation', 'to_entity', '*'), 'main', 'inlined')
       
  1038 
       
  1039 _AFFK = uicfg.autoform_field_kwargs
       
  1040 _AFFK.tag_attribute(('RQLExpression', 'expression'),
       
  1041                     {'widget': fw.TextInput})
       
  1042 _AFFK.tag_subject_of(('TrInfo', 'wf_info_for', '*'),
       
  1043                      {'widget': fw.HiddenInput})
       
  1044 
       
  1045 def registration_callback(vreg):
       
  1046     global etype_relation_field
       
  1047 
       
  1048     def etype_relation_field(etype, rtype, role='subject'):
       
  1049         try:
       
  1050             eschema = vreg.schema.eschema(etype)
       
  1051             return AutomaticEntityForm.field_by_name(rtype, role, eschema)
       
  1052         except (KeyError, f.FieldNotFound):
       
  1053             # catch KeyError raised when etype/rtype not found in schema
       
  1054             AutomaticEntityForm.error('field for %s %s may not be found in schema' % (rtype, role))
       
  1055             return None
       
  1056 
       
  1057     vreg.register_all(globals().values(), __name__)