merge
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 20 Jan 2010 10:14:14 +0100
changeset 4282 149b33d22d87
parent 4281 74c1597f8a82 (diff)
parent 4275 1084aaa53a25 (current diff)
child 4283 b3c40ae2d045
merge
--- a/doc/book/en/development/devweb/internationalization.rst	Wed Jan 20 10:05:10 2010 +0100
+++ b/doc/book/en/development/devweb/internationalization.rst	Wed Jan 20 10:14:14 2010 +0100
@@ -87,15 +87,20 @@
 
 May generate the following message ::
 
-  creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
+  add Execution has_export File subject
 
 This message will be used in views of ``EntityA`` for creation of a new
 ``EntityB`` with a preset relation ``relation_a2b`` between the current
 ``EntityA`` and the new ``EntityB``. The opposite message ::
 
-  creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
+  add Execution has_export File object
 
-Is used for similar creation of an ``EntityA`` from a view of ``EntityB``.
+Is used for similar creation of an ``EntityA`` from a view of ``EntityB``. The
+title of they respective creation form will be ::
+
+  creating EntityB (EntityA %(linkto)s relation_a2b EntityB)
+
+  creating EntityA (EntityA relation_a2b %(linkto)s EntityA)
 
 In the translated string you can use ``%(linkto)s`` for reference to the source
 ``entity``.
--- a/doc/book/en/development/webstdlib/autoform.rst	Wed Jan 20 10:05:10 2010 +0100
+++ b/doc/book/en/development/webstdlib/autoform.rst	Wed Jan 20 10:14:14 2010 +0100
@@ -1,8 +1,25 @@
 The automatic entity form (:mod:`cubicweb.web.views.autoform`)
 ---------------------------------------------------------------
 
+Tags declaration
+~~~~~~~~~~~~~~~~~~~~
+
 It is possible to manage attributes/relations in the simple or multiple
-editing form thanks to the following *rtags*:
+editing form thanks of the methods bellow ::
+
+  uicfg.autoform_section.tag_subject_of(<relation>, tag)
+  uicfg.autoform_section.tag_object_of(<relation>, tag)
+  uicfg.autoform_field.tag_attribute(<attribut_def>, tag)
+
+Where ``<relation>`` is a three elements tuple ``(Subject Entity Type,
+relation_type, Object Entity Type)``. ``<attribut_def>`` is a two elements tuple
+``(Entity Type, Attribut Name)``. Wildcard ``*`` could be used in place of
+``Entity Type``
+
+Possible tags are detailled below
+
+Simple Tags
+~~~~~~~~~~~~~~~~~~~~
 
 * `primary`, indicates that an attribute or a relation has to be
   inserted **in the simple or multiple editing forms**. In the case of
@@ -29,3 +46,24 @@
 If necessary, it is possible to overwrite the method
 `relation_category(rtype, x='subject')` to dynamically compute
 a relation editing category.
+
+
+Advanced Tags
+~~~~~~~~~~~~~~~~~~~~
+
+Tag can also reference a custom Field crafted with the help of
+``cubicweb.web.formfields`` and ``cubicweb.web.formwidget``. In the example
+bellow, the field ``path`` of ``ExecData`` entities will be done with a standard
+file input dialogue ::
+
+  from cubicweb.web import uicfg, formfields, formwidgets
+
+  uicfg.autoform_field.tag_attribute(('Execdata', 'path'),
+      formfields.FileField(name='path', widget=formwidgets.FileInput()))
+
+
+
+
+
+
+
--- a/web/controller.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/controller.py	Wed Jan 20 10:14:14 2010 +0100
@@ -35,19 +35,6 @@
             params[navparam] = form[redirectparam]
     return params
 
-def parse_relations_descr(rdescr):
-    """parse a string describing some relations, in the form
-    subjeids:rtype:objeids
-    where subjeids and objeids are eids separeted by a underscore
-
-    return an iterator on (subject eid, relation type, object eid) found
-    """
-    for rstr in rdescr:
-        subjs, rtype, objs = rstr.split(':')
-        for subj in subjs.split('_'):
-            for obj in objs.split('_'):
-                yield typed_eid(subj), rtype, typed_eid(obj)
-
 def append_url_params(url, params):
     """append raw parameters to the url. Given parameters, if any, are expected
     to be already url-quoted.
@@ -137,22 +124,6 @@
         else:
             self._cw.set_message(self._cw._('entity deleted'))
 
-    def delete_relations(self, rdefs):
-        """delete relations from the repository"""
-        # FIXME convert to using the syntax subject:relation:eids
-        execute = self._cw.execute
-        for subj, rtype, obj in rdefs:
-            rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
-            execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
-        self._cw.set_message(self._cw._('relations deleted'))
-
-    def insert_relations(self, rdefs):
-        """insert relations into the repository"""
-        execute = self._cw.execute
-        for subj, rtype, obj in rdefs:
-            rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
-            execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
-
 
     def reset(self):
         """reset form parameters and redirect to a view determinated by given
--- a/web/data/cubicweb.css	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/data/cubicweb.css	Wed Jan 20 10:14:14 2010 +0100
@@ -841,7 +841,7 @@
   font-weight: bold;
 }
 
-input.validateButton {
+.validateButton {
   margin: 1em 1em 0px 0px;
   border: 1px solid #edecd2;
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
--- a/web/data/cubicweb.edition.js	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/data/cubicweb.edition.js	Wed Jan 20 10:14:14 2010 +0100
@@ -399,14 +399,14 @@
 /* unfreeze form buttons when the validation process is over*/
 function unfreezeFormButtons(formid) {
     jQuery('#progress').hide();
-    jQuery('#' + formid + ' input.validateButton').removeAttr('disabled');
+    jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
     return true;
 }
 
 /* disable form buttons while the validation is being done */
 function freezeFormButtons(formid) {
     jQuery('#progress').show();
-    jQuery('#' + formid + ' input.validateButton').attr('disabled', 'disabled');
+    jQuery('#' + formid + ' .validateButton').attr('disabled', 'disabled');
     return true;
 }
 
--- a/web/data/cubicweb.form.css	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/data/cubicweb.form.css	Wed Jan 20 10:14:14 2010 +0100
@@ -108,7 +108,7 @@
   background: url("required.png") 100% 50% no-repeat;
 }
 
-.entityForm input.validateButton {
+.entityForm .validateButton {
   margin: 5px 10px 5px 0px;
 }
 
@@ -226,7 +226,7 @@
   cursor: default;
 }
 
-input.validateButton {
+.validateButton {
   margin: 1em 1em 0px 0px;
   border: 1px solid #edecd2;
   border-color:#edecd2 #cfceb7 #cfceb7  #edecd2;
--- a/web/data/cubicweb.preferences.js	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/data/cubicweb.preferences.js	Wed Jan 20 10:14:14 2010 +0100
@@ -134,7 +134,7 @@
     jQuery('form').each(function() {
 	var form = jQuery(this);
 	//freezeFormButtons(form.attr('id'));
-	form.find('input.validateButton').attr('disabled', 'disabled');
+	form.find('.validateButton').attr('disabled', 'disabled');
 	form.find('input[type=text]').keyup(function(){
 	    checkValues(form);
 	});
--- a/web/request.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/request.py	Wed Jan 20 10:14:14 2010 +0100
@@ -386,37 +386,7 @@
             raise RequestError(self._('missing parameters for entity %s') % eid)
         return params
 
-    def get_pending_operations(self, entity, relname, role):
-        operations = {'insert' : [], 'delete' : []}
-        for optype in ('insert', 'delete'):
-            data = self.get_session_data('pending_%s' % optype) or ()
-            for eidfrom, rel, eidto in data:
-                if relname == rel:
-                    if role == 'subject' and entity.eid == eidfrom:
-                        operations[optype].append(eidto)
-                    if role == 'object' and entity.eid == eidto:
-                        operations[optype].append(eidfrom)
-        return operations
-
-    def get_pending_inserts(self, eid=None):
-        """shortcut to access req's pending_insert entry
-
-        This is where are stored relations being added while editing
-        an entity. This used to be stored in a temporary cookie.
-        """
-        pending = self.get_session_data('pending_insert') or ()
-        return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
-                if eid is None or eid in (subj, obj)]
-
-    def get_pending_deletes(self, eid=None):
-        """shortcut to access req's pending_delete entry
-
-        This is where are stored relations being removed while editing
-        an entity. This used to be stored in a temporary cookie.
-        """
-        pending = self.get_session_data('pending_delete') or ()
-        return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
-                if eid is None or eid in (subj, obj)]
+    # XXX this should go to the GenericRelationsField. missing edition cancel protocol.
 
     def remove_pending_operations(self):
         """shortcut to clear req's pending_{delete,insert} entries
--- a/web/test/unittest_application.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/test/unittest_application.py	Wed Jan 20 10:14:14 2010 +0100
@@ -184,9 +184,9 @@
         values = forminfo['values']
         self.assertEquals(values['login-subject:'+eid], '')
         self.assertEquals(values['eid'], eid)
-        errors = forminfo['errors']
-        self.assertEquals(errors.entity, user.eid)
-        self.assertEquals(errors.errors['login'], 'required attribute')
+        error = forminfo['error']
+        self.assertEquals(error.entity, user.eid)
+        self.assertEquals(error.errors['login'], 'required attribute')
 
 
     def test_validation_error_dont_loose_subentity_data(self):
@@ -212,9 +212,9 @@
         path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
         forminfo = req.get_session_data('view?vid=edition...')
         self.assertEquals(set(forminfo['eidmap']), set('XY'))
-        self.assertEquals(forminfo['errors'].entity, forminfo['eidmap']['X'])
-        self.assertEquals(forminfo['errors'].errors, {'login': 'required attribute',
-                                                      'upassword': 'required attribute'})
+        self.assertEquals(forminfo['error'].entity, forminfo['eidmap']['X'])
+        self.assertEquals(forminfo['error'].errors, {'login': 'required attribute',
+                                                     'upassword': 'required attribute'})
         self.assertEquals(forminfo['values'], form)
 
     def _test_cleaned(self, kwargs, injected, cleaned):
--- a/web/views/autoform.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/autoform.py	Wed Jan 20 10:14:14 2010 +0100
@@ -9,12 +9,12 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.common.decorators import cached
+from logilab.common.decorators import cached, iclassmethod
 
 from cubicweb import typed_eid
 from cubicweb.web import stdmsgs, uicfg
 from cubicweb.web import form, formwidgets as fwdgs
-from cubicweb.web.views import forms, editforms
+from cubicweb.web.views import forms, editforms, editviews
 
 _afs = uicfg.autoform_section
 
@@ -44,6 +44,31 @@
     # which relations should be edited
     display_fields = None
 
+    def _generic_relations_field(self):
+        try:
+            srels_by_cat = self.srelations_by_category('generic', 'add', strict=True)
+            warn('[3.6] %s: srelations_by_category is deprecated, use uicfg or '
+                 'override editable_relations instead' % classid(form),
+                 DeprecationWarning)
+        except AttributeError:
+            srels_by_cat = self.editable_relations()
+        if not srels_by_cat:
+            raise form.FieldNotFound('_cw_generic_field')
+        return editviews.GenericRelationsField(self.editable_relations())
+
+    @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 form.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):
@@ -64,6 +89,12 @@
                 except form.FieldNotFound:
                     # meta attribute such as <attr>_format
                     continue
+        if self.formtype == 'main' and entity.has_eid():
+            try:
+                self.fields.append(self.field_by_name('_cw_generic_field'))
+            except form.FieldNotFound:
+                # no editable relation
+                pass
         self.maxrelitems = self._cw.property_value('navigation.related-limit')
         self.force_display = bool(self._cw.form.get('__force_display'))
         fnum = len(self.fields)
@@ -163,63 +194,6 @@
 
     # generic relations modifier ###############################################
 
-    def relations_table(self):
-        """yiels 3-tuples (rtype, target, related_list)
-        where <related_list> itself a list of :
-          - node_id (will be the entity element's DOM id)
-          - appropriate javascript's togglePendingDelete() function call
-          - status 'pendingdelete' or ''
-          - oneline view of related entity
-        """
-        entity = self.edited_entity
-        pending_deletes = self._cw.get_pending_deletes(entity.eid)
-        for label, rschema, role in self.editable_relations():
-            relatedrset = entity.related(rschema, role, limit=self.related_limit)
-            if rschema.has_perm(self._cw, 'delete'):
-                toggleable_rel_link_func = editforms.toggleable_relation_link
-            else:
-                toggleable_rel_link_func = lambda x, y, z: u''
-            related = []
-            for row in xrange(relatedrset.rowcount):
-                nodeid = editforms.relation_id(entity.eid, rschema, role,
-                                               relatedrset[row][0])
-                if nodeid in pending_deletes:
-                    status = u'pendingDelete'
-                    label = '+'
-                else:
-                    status = u''
-                    label = 'x'
-                dellink = toggleable_rel_link_func(entity.eid, nodeid, label)
-                eview = self._cw.view('oneline', relatedrset, row=row)
-                related.append((nodeid, dellink, status, eview))
-            yield (rschema, role, related)
-
-    def restore_pending_inserts(self, cell=False):
-        """used to restore edition page as it was before clicking on
-        'search for <some entity type>'
-        """
-        eid = self.edited_entity.eid
-        cell = cell and "div_insert_" or "tr"
-        pending_inserts = set(self._cw.get_pending_inserts(eid))
-        for pendingid in pending_inserts:
-            eidfrom, rtype, eidto = pendingid.split(':')
-            if typed_eid(eidfrom) == eid: # subject
-                label = display_name(self._cw, rtype, 'subject',
-                                     self.edited_entity.__regid__)
-                reid = eidto
-            else:
-                label = display_name(self._cw, rtype, 'object',
-                                     self.edited_entity.__regid__)
-                reid = eidfrom
-            jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
-                     % (pendingid, cell, eid)
-            rset = self._cw.eid_rset(reid)
-            eview = self._cw.view('text', rset, row=0)
-            # XXX find a clean way to handle baskets
-            if rset.description[0][0] == 'Basket':
-                eview = '%s (%s)' % (eview, display_name(self._cw, 'Basket'))
-            yield rtype, pendingid, jscall, label, reid, eview
-
     # inlined forms support ####################################################
 
     @cached
--- a/web/views/basetemplates.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/basetemplates.py	Wed Jan 20 10:14:14 2010 +0100
@@ -266,7 +266,10 @@
             self.w(u'</tr></table>\n')
 
 if can_do_pdf_conversion():
-    from xml.etree.cElementTree import ElementTree
+    try:
+      from xml.etree.cElementTree import ElementTree
+    except ImportError: #python2.4
+        from elementtree import ElementTree
     from subprocess import Popen as sub
     from StringIO import StringIO
     from tempfile import NamedTemporaryFile
--- a/web/views/editcontroller.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/editcontroller.py	Wed Jan 20 10:14:14 2010 +0100
@@ -13,7 +13,7 @@
 
 from cubicweb import Binary, ValidationError, typed_eid
 from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
-from cubicweb.web.controller import parse_relations_descr
+from cubicweb.web.views.editviews import delete_relations, insert_relations
 from cubicweb.web.views.basecontrollers import ViewController
 
 
@@ -73,8 +73,6 @@
         # no specific action, generic edition
         self._to_create = req.data['eidmap'] = {}
         self._pending_fields = req.data['pendingfields'] = set()
-        todelete = self._cw.get_pending_deletes()
-        toinsert = self._cw.get_pending_inserts()
         try:
             methodname = req.form.pop('__method', None)
             for eid in req.edited_eids():
@@ -88,7 +86,7 @@
         except (RequestError, NothingToEdit), ex:
             if '__linkto' in req.form and 'eid' in req.form:
                 self.execute_linkto()
-            elif not ('__delete' in req.form or '__insert' in req.form or todelete or toinsert):
+            elif not ('__delete' in req.form or '__insert' in req.form):
                 raise ValidationError(None, {None: unicode(ex)})
         # handle relations in newly created entities
         if self._pending_fields:
@@ -99,13 +97,15 @@
             self._cw.execute(*querydef)
         # XXX this processes *all* pending operations of *all* entities
         if req.form.has_key('__delete'):
-            todelete += req.list_form_param('__delete', req.form, pop=True)
-        if todelete:
-            self.delete_relations(parse_relations_descr(todelete))
+            todelete = req.list_form_param('__delete', req.form, pop=True)
+            if todelete:
+                delete_relations(self._cw, todelete)
         if req.form.has_key('__insert'):
+            warn('[3.6] stop using __insert, support will be removed',
+                 DeprecationWarning)
             toinsert = req.list_form_param('__insert', req.form, pop=True)
-        if toinsert:
-            self.insert_relations(parse_relations_descr(toinsert))
+            if toinsert:
+                insert_relations(self._cw, toinsert)
         self._cw.remove_pending_operations()
         if self.errors:
             errors = dict((f.name, unicode(ex)) for f, ex in self.errors)
@@ -171,13 +171,11 @@
         if is_main_entity:
             self.notify_edited(entity)
         if formparams.has_key('__delete'):
+            # XXX deprecate?
             todelete = self._cw.list_form_param('__delete', formparams, pop=True)
-            self.delete_relations(parse_relations_descr(todelete))
+            delete_relations(self._cw, todelete)
         if formparams.has_key('__cloned_eid'):
             entity.copy_relations(typed_eid(formparams['__cloned_eid']))
-        if formparams.has_key('__insert'):
-            toinsert = self._cw.list_form_param('__insert', formparams, pop=True)
-            self.insert_relations(parse_relations_descr(toinsert))
         if is_main_entity: # only execute linkto for the main entity
             self.execute_linkto(entity.eid)
         return eid
--- a/web/views/editforms.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/editforms.py	Wed Jan 20 10:14:14 2010 +0100
@@ -21,8 +21,7 @@
                                 specified_etype_implements, yes)
 from cubicweb.view import EntityView
 from cubicweb import tags
-from cubicweb.web import stdmsgs, eid_param
-from cubicweb.web import uicfg
+from cubicweb.web import uicfg, stdmsgs, eid_param
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
 from cubicweb.web.formfields import guess_field
 from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton
@@ -30,21 +29,6 @@
 
 _pvdc = uicfg.primaryview_display_ctrl
 
-def relation_id(eid, rtype, role, reid):
-    """return an identifier for a relation between two entities"""
-    if role == 'subject':
-        return u'%s:%s:%s' % (eid, rtype, reid)
-    return u'%s:%s:%s' % (reid, rtype, eid)
-
-def toggleable_relation_link(eid, nodeid, label='x'):
-    """return javascript snippet to delete/undelete a relation between two
-    entities
-    """
-    js = u"javascript: togglePendingDelete('%s', %s);" % (
-        nodeid, xml_escape(dumps(eid)))
-    return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
-        js, nodeid, label)
-
 
 class DeleteConfForm(forms.CompositeForm):
     __regid__ = 'deleteconf'
--- a/web/views/editviews.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/editviews.py	Wed Jan 20 10:14:14 2010 +0100
@@ -13,14 +13,29 @@
 from logilab.common.decorators import cached
 from logilab.mtconverter import xml_escape
 
-from cubicweb import typed_eid
+from cubicweb import typed_eid, uilib
+from cubicweb.schema import display_name
 from cubicweb.view import EntityView
 from cubicweb.selectors import (one_line_rset, non_final_entity,
                                 match_search_state, match_form_params)
-from cubicweb.uilib import cut
-from cubicweb.web.views import linksearch_select_url
-from cubicweb.web.views.editforms import relation_id
-from cubicweb.web.views.baseviews import FinalView
+from cubicweb.web import formwidgets as fw, formfields as ff
+from cubicweb.web.views import baseviews, linksearch_select_url
+
+
+def relation_id(eid, rtype, role, reid):
+    """return an identifier for a relation between two entities"""
+    if role == 'subject':
+        return u'%s:%s:%s' % (eid, rtype, reid)
+    return u'%s:%s:%s' % (reid, rtype, eid)
+
+def toggleable_relation_link(eid, nodeid, label='x'):
+    """return javascript snippet to delete/undelete a relation between two
+    entities
+    """
+    js = u"javascript: togglePendingDelete('%s', %s);" % (
+        nodeid, xml_escape(dumps(eid)))
+    return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
+        js, nodeid, label)
 
 
 class SearchForAssociationView(EntityView):
@@ -73,37 +88,226 @@
             entity.view('outofcontext', w=self.w)
 
 
+def get_pending_inserts(req, eid=None):
+    """shortcut to access req's pending_insert entry
+
+    This is where are stored relations being added while editing
+    an entity. This used to be stored in a temporary cookie.
+    """
+    pending = req.get_session_data('pending_insert') or ()
+    return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
+            if eid is None or eid in (subj, obj)]
+
+def get_pending_deletes(req, eid=None):
+    """shortcut to access req's pending_delete entry
+
+    This is where are stored relations being removed while editing
+    an entity. This used to be stored in a temporary cookie.
+    """
+    pending = req.get_session_data('pending_delete') or ()
+    return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending
+            if eid is None or eid in (subj, obj)]
+
+def parse_relations_descr(rdescr):
+    """parse a string describing some relations, in the form
+    subjeids:rtype:objeids
+    where subjeids and objeids are eids separeted by a underscore
+
+    return an iterator on (subject eid, relation type, object eid) found
+    """
+    for rstr in rdescr:
+        subjs, rtype, objs = rstr.split(':')
+        for subj in subjs.split('_'):
+            for obj in objs.split('_'):
+                yield typed_eid(subj), rtype, typed_eid(obj)
+
+def delete_relations(req, rdefs):
+    """delete relations from the repository"""
+    # FIXME convert to using the syntax subject:relation:eids
+    execute = req.execute
+    for subj, rtype, obj in parse_relations_descr(rdefs):
+        rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
+        execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
+    req.set_message(req._('relations deleted'))
+
+def insert_relations(req, rdefs):
+    """insert relations into the repository"""
+    execute = req.execute
+    for subj, rtype, obj in parse_relations_descr(rdefs):
+        rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
+        execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
+
+
+
+class GenericRelationsWidget(fw.FieldWidget):
+
+    def render(self, form, field, renderer):
+        stream = []
+        w = stream.append
+        req = form._cw
+        _ = req._
+        __ = _
+        label = u'%s :' % __('This %s' % form.edited_entity.e_schema).capitalize()
+        eid = form.edited_entity.eid
+        w(u'<fieldset class="subentity">')
+        w(u'<legend class="iformTitle">%s</legend>' % label)
+        w(u'<table id="relatedEntities">')
+        for rschema, role, related in field.relations_table(form):
+            # already linked entities
+            if related:
+                w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, role))
+                w(u'<td>')
+                w(u'<ul>')
+                for viewparams in related:
+                    w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
+                      % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
+                if not form.force_display and form.maxrelitems < len(related):
+                    link = (u'<span class="invisible">'
+                            '[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
+                            '</span>' % self._cw._('view all'))
+                    w(u'<li class="invisible">%s</li>' % link)
+                w(u'</ul>')
+                w(u'</td>')
+                w(u'</tr>')
+        pendings = list(field.restore_pending_inserts(form))
+        if not pendings:
+            w(u'<tr><th>&#160;</th><td>&#160;</td></tr>')
+        else:
+            for row in pendings:
+                # soon to be linked to entities
+                w(u'<tr id="tr%s">' % row[1])
+                w(u'<th>%s</th>' % row[3])
+                w(u'<td>')
+                w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
+                  (_('cancel this insert'), row[2]))
+                w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
+                  % (row[1], row[4], xml_escape(row[5])))
+                w(u'</td>')
+                w(u'</tr>')
+        w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
+        w(u'<th class="labelCol">')
+        w(u'<select id="relationSelector_%s" tabindex="%s" '
+          'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
+          % (eid, req.next_tabindex(), xml_escape(dumps(eid))))
+        w(u'<option value="">%s</option>' % _('select a relation'))
+        for i18nrtype, rschema, role in field.relations:
+            # more entities to link to
+            w(u'<option value="%s_%s">%s</option>' % (rschema, role, i18nrtype))
+        w(u'</select>')
+        w(u'</th>')
+        w(u'<td id="unrelatedDivs_%s"></td>' % eid)
+        w(u'</tr>')
+        w(u'</table>')
+        w(u'</fieldset>')
+        return '\n'.join(stream)
+
+
+class GenericRelationsField(ff.Field):
+    widget = GenericRelationsWidget
+
+    def __init__(self, relations, name='_cw_generic_field', **kwargs):
+        assert relations
+        kwargs['eidparam'] = True
+        super(GenericRelationsField, self).__init__(name, **kwargs)
+        self.relations = relations
+        self.label = None
+
+    def process_posted(self, form):
+        todelete = get_pending_deletes(form._cw)
+        if todelete:
+            delete_relations(form._cw, todelete)
+        toinsert = get_pending_inserts(form._cw)
+        if toinsert:
+            insert_relations(form._cw, toinsert)
+        return ()
+
+    def relations_table(self, form):
+        """yiels 3-tuples (rtype, role, related_list)
+        where <related_list> itself a list of :
+          - node_id (will be the entity element's DOM id)
+          - appropriate javascript's togglePendingDelete() function call
+          - status 'pendingdelete' or ''
+          - oneline view of related entity
+        """
+        entity = form.edited_entity
+        pending_deletes = get_pending_deletes(form._cw, entity.eid)
+        for label, rschema, role in self.relations:
+            related = []
+            if entity.has_eid():
+                rset = entity.related(rschema, role, limit=form.related_limit)
+                if rschema.has_perm(form._cw, 'delete'):
+                    toggleable_rel_link_func = toggleable_relation_link
+                else:
+                    toggleable_rel_link_func = lambda x, y, z: u''
+                for row in xrange(rset.rowcount):
+                    nodeid = relation_id(entity.eid, rschema, role,
+                                         rset[row][0])
+                    if nodeid in pending_deletes:
+                        status, label = u'pendingDelete', '+'
+                    else:
+                        status, label = u'', 'x'
+                    dellink = toggleable_rel_link_func(entity.eid, nodeid, label)
+                    eview = form._cw.view('oneline', rset, row=row)
+                    related.append((nodeid, dellink, status, eview))
+            yield (rschema, role, related)
+
+    def restore_pending_inserts(self, form):
+        """used to restore edition page as it was before clicking on
+        'search for <some entity type>'
+        """
+        entity = form.edited_entity
+        pending_inserts = set(get_pending_inserts(form._cw, form.edited_entity.eid))
+        for pendingid in pending_inserts:
+            eidfrom, rtype, eidto = pendingid.split(':')
+            if typed_eid(eidfrom) == entity.eid: # subject
+                label = display_name(form._cw, rtype, 'subject',
+                                     entity.__regid__)
+                reid = eidto
+            else:
+                label = display_name(form._cw, rtype, 'object',
+                                     entity.__regid__)
+                reid = eidfrom
+            jscall = "javascript: cancelPendingInsert('%s', 'tr', null, %s);" \
+                     % (pendingid, entity.eid)
+            rset = form._cw.eid_rset(reid)
+            eview = form._cw.view('text', rset, row=0)
+            # XXX find a clean way to handle baskets
+            if rset.description[0][0] == 'Basket':
+                eview = '%s (%s)' % (eview, display_name(form._cw, 'Basket'))
+            yield rtype, pendingid, jscall, label, reid, eview
+
+
 class UnrelatedDivs(EntityView):
     __regid__ = 'unrelateddivs'
     __select__ = match_form_params('relation')
 
     def cell_call(self, row, col):
         entity = self.cw_rset.get_entity(row, col)
-        relname, target = self._cw.form.get('relation').rsplit('_', 1)
+        relname, role = self._cw.form.get('relation').rsplit('_', 1)
         rschema = self._cw.vreg.schema.rschema(relname)
         hidden = 'hidden' in self._cw.form
         is_cell = 'is_cell' in self._cw.form
-        self.w(self.build_unrelated_select_div(entity, rschema, target,
+        self.w(self.build_unrelated_select_div(entity, rschema, role,
                                                is_cell=is_cell, hidden=hidden))
 
-    def build_unrelated_select_div(self, entity, rschema, target,
+    def build_unrelated_select_div(self, entity, rschema, role,
                                    is_cell=False, hidden=True):
         options = []
-        divid = 'div%s_%s_%s' % (rschema.type, target, entity.eid)
-        selectid = 'select%s_%s_%s' % (rschema.type, target, entity.eid)
-        if rschema.symetric or target == 'subject':
+        divid = 'div%s_%s_%s' % (rschema.type, role, entity.eid)
+        selectid = 'select%s_%s_%s' % (rschema.type, role, entity.eid)
+        if rschema.symetric or role == 'subject':
             targettypes = rschema.objects(entity.e_schema)
             etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes))
         else:
             targettypes = rschema.subjects(entity.e_schema)
             etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes))
-        etypes = cut(etypes, self._cw.property_value('navigation.short-line-size'))
+        etypes = uilib.cut(etypes, self._cw.property_value('navigation.short-line-size'))
         options.append('<option>%s %s</option>' % (self._cw._('select a'), etypes))
-        options += self._get_select_options(entity, rschema, target)
-        options += self._get_search_options(entity, rschema, target, targettypes)
+        options += self._get_select_options(entity, rschema, role)
+        options += self._get_search_options(entity, rschema, role, targettypes)
         if 'Basket' in self._cw.vreg.schema: # XXX
-            options += self._get_basket_options(entity, rschema, target, targettypes)
-        relname, target = self._cw.form.get('relation').rsplit('_', 1)
+            options += self._get_basket_options(entity, rschema, role, targettypes)
+        relname, role = self._cw.form.get('relation').rsplit('_', 1)
         return u"""\
 <div class="%s" id="%s">
   <select id="%s" onchange="javascript: addPendingInsert(this.options[this.selectedIndex], %s, %s, '%s');">
@@ -114,32 +318,33 @@
        xml_escape(dumps(entity.eid)), is_cell and 'true' or 'null', relname,
        '\n'.join(options))
 
-    def _get_select_options(self, entity, rschema, target):
+    def _get_select_options(self, entity, rschema, role):
         """add options to search among all entities of each possible type"""
         options = []
-        pending_inserts = self._cw.get_pending_inserts(entity.eid)
+        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, target, entity.e_schema)
+        field = form.field_by_name(rschema, role, entity.e_schema)
         limit = self._cw.property_value('navigation.combobox-limit')
         for eview, reid in field.choices(form, limit): # XXX expect 'limit' arg on choices
             if reid is None:
-                options.append('<option class="separator">-- %s --</option>'
-                               % xml_escape(eview))
+                if eview: # skip blank value
+                    options.append('<option class="separator">-- %s --</option>'
+                                   % xml_escape(eview))
             else:
-                optionid = relation_id(entity.eid, rtype, target, reid)
+                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('<option id="id%s" value="%s">%s</option>' %
                                    (optionid, reid, xml_escape(eview)))
         return options
 
-    def _get_search_options(self, entity, rschema, target, targettypes):
+    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' % (target, entity.eid, rschema.type, eschema)
+            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),
@@ -147,18 +352,18 @@
                 xml_escape(url), _('Search for'), eschema.display_name(self._cw))))
         return [o for l, o in sorted(options)]
 
-    def _get_basket_options(self, entity, rschema, target, targettypes):
+    def _get_basket_options(self, entity, rschema, role, targettypes):
         options = []
         rtype = rschema.type
         _ = self._cw._
         for basketeid, basketname in self._get_basket_links(self._cw.user.eid,
-                                                            target, targettypes):
-            optionid = relation_id(entity.eid, rtype, target, basketeid)
+                                                            role, targettypes):
+            optionid = relation_id(entity.eid, rtype, role, basketeid)
             options.append('<option id="%s" value="%s">%s %s</option>' % (
                 optionid, basketeid, _('link to each item in'), xml_escape(basketname)))
         return options
 
-    def _get_basket_links(self, ueid, target, targettypes):
+    def _get_basket_links(self, ueid, role, targettypes):
         targettypes = set(targettypes)
         for basketeid, basketname, elements in self._get_basket_info(ueid):
             baskettypes = elements.column_types(0)
@@ -193,7 +398,7 @@
         self.wview('textoutofcontext', self.cw_rset, row=row, col=col)
 
 
-class EditableFinalView(FinalView):
+class EditableFinalView(baseviews.FinalView):
     """same as FinalView but enables inplace-edition when possible"""
     __regid__ = 'editable-final'
 
--- a/web/views/formrenderers.py	Wed Jan 20 10:05:10 2010 +0100
+++ b/web/views/formrenderers.py	Wed Jan 20 10:14:14 2010 +0100
@@ -1,7 +1,7 @@
 """form renderers, responsible to layout a form to html
 
 :organization: Logilab
-:copyright: 2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
@@ -205,14 +205,15 @@
             w(u'<table class="%s">' % self.table_class)
             for field in fields:
                 w(u'<tr class="%s_%s_row">' % (field.name, field.role))
-                if self.display_label:
+                if self.display_label and field.label is not None:
                     w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
+                w('<td')
+                if field.label is None:
+                    w(' colspan="2"')
                 error = form.field_error(field)
                 if error:
-                    w(u'<td class="error">')
-                    self.render_error(w, error)
-                else:
-                    w(u'<td>')
+                    w(u' class="error"')
+                w(u'>')
                 w(field.render(form, self))
                 if self.display_help:
                     w(self.render_help(form, field))
@@ -392,72 +393,6 @@
  </table>""" % tuple(button.render(form) for button in form.form_buttons))
         else:
             super(EntityFormRenderer, self).render_buttons(w, form)
-
-    def relations_form(self, w, form):
-        try:
-            srels_by_cat = form.srelations_by_category('generic', 'add', strict=True)
-            warn('[3.6] %s: srelations_by_category is deprecated, override '
-                 'editable_relations instead' % classid(form), DeprecationWarning)
-        except AttributeError:
-            srels_by_cat = form.editable_relations()
-        if not srels_by_cat:
-            return u''
-        req = self._cw
-        _ = req._
-        __ = _
-        label = u'%s :' % __('This %s' % form.edited_entity.e_schema).capitalize()
-        eid = form.edited_entity.eid
-        w(u'<fieldset class="subentity">')
-        w(u'<legend class="iformTitle">%s</legend>' % label)
-        w(u'<table id="relatedEntities">')
-        for rschema, target, related in form.relations_table():
-            # already linked entities
-            if related:
-                w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target))
-                w(u'<td>')
-                w(u'<ul>')
-                for viewparams in related:
-                    w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
-                      % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
-                if not form.force_display and form.maxrelitems < len(related):
-                    link = (u'<span class="invisible">'
-                            '[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
-                            '</span>' % self._cw._('view all'))
-                    w(u'<li class="invisible">%s</li>' % link)
-                w(u'</ul>')
-                w(u'</td>')
-                w(u'</tr>')
-        pendings = list(form.restore_pending_inserts())
-        if not pendings:
-            w(u'<tr><th>&#160;</th><td>&#160;</td></tr>')
-        else:
-            for row in pendings:
-                # soon to be linked to entities
-                w(u'<tr id="tr%s">' % row[1])
-                w(u'<th>%s</th>' % row[3])
-                w(u'<td>')
-                w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
-                  (_('cancel this insert'), row[2]))
-                w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
-                  % (row[1], row[4], xml_escape(row[5])))
-                w(u'</td>')
-                w(u'</tr>')
-        w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
-        w(u'<th class="labelCol">')
-        w(u'<select id="relationSelector_%s" tabindex="%s" '
-          'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
-          % (eid, req.next_tabindex(), xml_escape(dumps(eid))))
-        w(u'<option value="">%s</option>' % _('select a relation'))
-        for i18nrtype, rschema, target in srels_by_cat:
-            # more entities to link to
-            w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype))
-        w(u'</select>')
-        w(u'</th>')
-        w(u'<td id="unrelatedDivs_%s"></td>' % eid)
-        w(u'</tr>')
-        w(u'</table>')
-        w(u'</fieldset>')
-
     # NOTE: should_* and display_* method extracted and moved to the form to
     # ease overriding