[form, entity] refactor '__linkto', now handled by the entity form, not the entity itself. Closes #1931543
authorFlorent Cayré <florent.cayre@gmail.com>
Tue, 27 Sep 2011 18:46:36 +0200
changeset 7875 65e460690139
parent 7871 29fc83fca803
child 7876 df15d194a134
[form, entity] refactor '__linkto', now handled by the entity form, not the entity itself. Closes #1931543
doc/3.14.rst
entities/__init__.py
web/formfields.py
web/views/forms.py
web/views/workflow.py
--- a/doc/3.14.rst	Mon Sep 26 19:46:04 2011 +0200
+++ b/doc/3.14.rst	Tue Sep 27 18:46:36 2011 +0200
@@ -36,3 +36,15 @@
   No backward compat yet since this is a protected method an no code is known to
   use it outside cubicweb itself.
 
+* `AnyEntity.linked_to` has been removed as part of a refactoring of this
+  functionality (link a entity to another one at creation step). It was replaced
+  by a `EntityFieldsForm.linked_to` property.
+
+  In the same refactoring, `cubicweb.web.formfield.relvoc_linkedto`,
+  `cubicweb.web.formfield.relvoc_init` and
+  `cubicweb.web.formfield.relvoc_unrelated` were removed and replaced by
+  RelationField methods with the same names, that take a form as a parameter.
+
+  **No backward compatibility yet**. It's still time to cry for it.
+  Cubes known to be affected: tracker, vcsfile, vcreview
+
--- a/entities/__init__.py	Mon Sep 26 19:46:04 2011 +0200
+++ b/entities/__init__.py	Tue Sep 27 18:46:36 2011 +0200
@@ -118,38 +118,6 @@
             return self.printable_value(rtype, format='text/plain').lower()
         return value
 
-    # edition helper functions ################################################
-
-    def linked_to(self, rtype, role, remove=True):
-        """if entity should be linked to another using '__linkto' form param for
-        the given relation/role, return eids of related entities
-
-        This method is consuming matching link-to information from form params
-        if `remove` is True (by default). Computed values are stored into a
-        `cw_linkto` attribute, a dictionary with (relation, role) as key and
-        linked eids as value.
-        """
-        try:
-            return self.cw_linkto[(rtype, role)]
-        except AttributeError:
-            self.cw_linkto = {}
-        except KeyError:
-            pass
-        linktos = list(self._cw.list_form_param('__linkto'))
-        linkedto = []
-        for linkto in linktos[:]:
-            ltrtype, eid, ltrole = linkto.split(':')
-            if rtype == ltrtype and role == ltrole:
-                # delete __linkto from form param to avoid it being added as
-                # hidden input
-                if remove:
-                    linktos.remove(linkto)
-                    self._cw.form['__linkto'] = linktos
-                linkedto.append(typed_eid(eid))
-        self.cw_linkto[(rtype, role)] = linkedto
-        return linkedto
-
-    # server side helpers #####################################################
 
 def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'):
     """function to ease basic configuration of an entity class ORM. Basic usage
--- a/web/formfields.py	Mon Sep 26 19:46:04 2011 +0200
+++ b/web/formfields.py	Tue Sep 27 18:46:36 2011 +0200
@@ -1018,59 +1018,6 @@
         return list(self.fields)
 
 
-# relation vocabulary helper functions #########################################
-
-def relvoc_linkedto(entity, rtype, role):
-    # first see if its specified by __linkto form parameters
-    linkedto = entity.linked_to(rtype, role)
-    if linkedto:
-        buildent = entity._cw.entity_from_eid
-        return [(buildent(eid).view('combobox'), unicode(eid)) for eid in linkedto]
-    return []
-
-def relvoc_init(entity, rtype, role, required=False):
-    # it isn't, check if the entity provides a method to get correct values
-    vocab = []
-    if not required:
-        vocab.append(('', INTERNAL_FIELD_VALUE))
-    # vocabulary doesn't include current values, add them
-    if entity.has_eid():
-        rset = entity.related(rtype, role)
-        vocab += [(e.view('combobox'), unicode(e.eid)) for e in rset.entities()]
-    return vocab
-
-def relvoc_unrelated(entity, rtype, role, limit=None):
-    if isinstance(rtype, basestring):
-        rtype = entity._cw.vreg.schema.rschema(rtype)
-    if entity.has_eid():
-        done = set(row[0] for row in entity.related(rtype, role))
-    else:
-        done = None
-    result = []
-    rsetsize = None
-    for objtype in rtype.targets(entity.e_schema, role):
-        if limit is not None:
-            rsetsize = limit - len(result)
-        result += _relvoc_unrelated(entity, rtype, objtype, role, rsetsize, done)
-        if limit is not None and len(result) >= limit:
-            break
-    return result
-
-def _relvoc_unrelated(entity, rtype, targettype, role, limit, done):
-    """return unrelated entities for a given relation and target entity type
-    for use in vocabulary
-    """
-    if done is None:
-        done = set()
-    res = []
-    for entity in entity.unrelated(rtype, targettype, role, limit).entities():
-        if entity.eid in done:
-            continue
-        done.add(entity.eid)
-        res.append((entity.view('combobox'), unicode(entity.eid)))
-    return res
-
-
 class RelationField(Field):
     """Use this field to edit a relation of an entity.
 
@@ -1095,10 +1042,10 @@
         entity = form.edited_entity
         # first see if its specified by __linkto form parameters
         if limit is None:
-            linkedto = relvoc_linkedto(entity, self.name, self.role)
+            linkedto = self.relvoc_linkedto(form)
             if linkedto:
                 return linkedto
-            vocab = relvoc_init(entity, self.name, self.role, self.required)
+            vocab = self.relvoc_init(form)
         else:
             vocab = []
         # it isn't, check if the entity provides a method to get correct values
@@ -1108,22 +1055,63 @@
             warn('[3.6] found %s on %s, should override field.choices instead (need tweaks)'
                  % (method, form), DeprecationWarning)
         except AttributeError:
-            vocab += relvoc_unrelated(entity, self.name, self.role, limit)
+            vocab += self.relvoc_unrelated(form, limit)
         if self.sort:
             vocab = vocab_sort(vocab)
         return vocab
 
-    def form_init(self, form):
-        #if not self.display_value(form):
-        value = form.edited_entity.linked_to(self.name, self.role)
-        if value:
-            searchedvalues = ['%s:%s:%s' % (self.name, eid, self.role)
-                              for eid in value]
-            # remove associated __linkto hidden fields
-            for field in form.root_form.fields_by_name('__linkto'):
-                if field.value in searchedvalues:
-                    form.root_form.remove_field(field)
-            form.formvalues[(self, form)] = value
+    def relvoc_linkedto(self, form):
+        linkedto = form.linked_to.get((self.name, self.role))
+        if linkedto:
+            buildent = form._cw.entity_from_eid
+            return [(buildent(eid).view('combobox'), unicode(eid))
+                    for eid in linkedto]
+        return []
+
+    def relvoc_init(self, form):
+        entity, rtype, role = form.edited_entity, self.name, self.role
+        vocab = []
+        if not self.required:
+            vocab.append(('', INTERNAL_FIELD_VALUE))
+        # vocabulary doesn't include current values, add them
+        if form.edited_entity.has_eid():
+            rset = form.edited_entity.related(self.name, self.role)
+            vocab += [(e.view('combobox'), unicode(e.eid))
+                      for e in rset.entities()]
+        return vocab
+
+    def relvoc_unrelated(self, form, limit=None):
+        entity = form.edited_entity
+        rtype = entity._cw.vreg.schema.rschema(self.name)
+        if entity.has_eid():
+            done = set(row[0] for row in entity.related(rtype, self.role))
+        else:
+            done = None
+        result = []
+        rsetsize = None
+        for objtype in rtype.targets(entity.e_schema, self.role):
+            if limit is not None:
+                rsetsize = limit - len(result)
+            result += self._relvoc_unrelated(form, objtype, rsetsize, done)
+            if limit is not None and len(result) >= limit:
+                break
+        return result
+
+    def _relvoc_unrelated(self, form, targettype, limit, done):
+        """return unrelated entities for a given relation and target entity type
+        for use in vocabulary
+        """
+        if done is None:
+            done = set()
+        res = []
+        entity = form.edited_entity
+        for entity in entity.unrelated(self.name, targettype,
+                                       self.role, limit).entities():
+            if entity.eid in done:
+                continue
+            done.add(entity.eid)
+            res.append((entity.view('combobox'), unicode(entity.eid)))
+        return res
 
     def format_single_value(self, req, value):
         return unicode(value)
--- a/web/views/forms.py	Mon Sep 26 19:46:04 2011 +0200
+++ b/web/views/forms.py	Tue Sep 27 18:46:36 2011 +0200
@@ -47,7 +47,7 @@
 from warnings import warn
 
 from logilab.common import dictattr, tempattr
-from logilab.common.decorators import iclassmethod
+from logilab.common.decorators import iclassmethod, cached
 from logilab.common.compat import any
 from logilab.common.textutils import splitstrip
 from logilab.common.deprecation import deprecated
@@ -57,7 +57,7 @@
 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import RequestError, ProcessFormError
 from cubicweb.web import uicfg, form, formwidgets as fwdgs
-from cubicweb.web.formfields import relvoc_unrelated, guess_field
+from cubicweb.web.formfields import guess_field
 
 
 class FieldsForm(form.Form):
@@ -376,9 +376,7 @@
         if kwargs.get('mainform', True) or kwargs.get('mainentity', False):
             self.add_hidden(u'__maineid', self.edited_entity.eid)
             # If we need to directly attach the new object to another one
-            if self._cw.list_form_param('__linkto'):
-                for linkto in self._cw.list_form_param('__linkto'):
-                    self.add_hidden('__linkto', linkto)
+            if '__linkto' in self._cw.form:
                 if msg:
                     msg = '%s %s' % (msg, self._cw._('and linked'))
                 else:
@@ -387,6 +385,38 @@
             msgid = self._cw.set_redirect_message(msg)
             self.add_hidden('_cwmsgid', msgid)
 
+    def add_linkto_hidden(self):
+        '''add the __linkto hidden field used to directly attach the new object
+        to an existing other one when the relation between those two is not
+        already present in the form.
+        Warning: this method must be called only when all form fields are setup'''
+        # if current form is not the main form, exit immediately
+        try:
+            self.field_by_name('__maineid')
+        except form.FieldNotFound:
+            return
+        for (rtype, role), eids in self.linked_to.iteritems():
+            # if the relation is already setup by a form field, do not add it
+            # in a __linkto hidden to avoid setting it twice in the controller
+            try:
+                self.field_by_name(rtype, role)
+            except form.FieldNotFound:
+                for eid in eids:
+                    self.add_hidden('__linkto', '%s:%s:%s' % (rtype, role, eid))
+
+    def render(self, *args, **kwargs):
+        self.add_linkto_hidden()
+        return super(EntityFieldsForm, self).render(*args, **kwargs)
+
+    @property
+    @cached
+    def linked_to(self):
+        linked_to = {}
+        for linkto in self._cw.list_form_param('__linkto'):
+            ltrtype, eid, ltrole = linkto.split(':')
+            linked_to.setdefault((ltrtype, ltrole), []).append(typed_eid(eid))
+        return linked_to
+
     def session_key(self):
         """return the key that may be used to store / retreive data about a
         previous post which failed because of a validation error
@@ -427,16 +457,18 @@
     def editable_relations(self):
         return ()
 
-    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
+    @deprecated('[3.6] use cw.web.formfields.RelationField.relvoc_unrelated method')
     def subject_relation_vocabulary(self, rtype, limit=None):
         """defaut vocabulary method for the given relation, looking for
         relation's object entities (i.e. self is the subject)
         """
-        return relvoc_unrelated(self.edited_entity, rtype, 'subject', limit=None)
+        field = self.field_by_name(rtype, 'subject')
+        return field.relvoc_unrelated(form, limit=None)
 
-    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
+    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated method')
     def object_relation_vocabulary(self, rtype, limit=None):
-        return relvoc_unrelated(self.edited_entity, rtype, 'object', limit=None)
+        field = self.field_by_name(rtype, 'object')
+        return field.relvoc_unrelated(form, limit=None)
 
 
 class CompositeFormMixIn(object):
--- a/web/views/workflow.py	Mon Sep 26 19:46:04 2011 +0200
+++ b/web/views/workflow.py	Tue Sep 27 18:46:36 2011 +0200
@@ -340,7 +340,7 @@
 def transition_states_vocabulary(form, field):
     entity = form.edited_entity
     if not entity.has_eid():
-        eids = entity.linked_to('transition_of', 'subject')
+        eids = form.linked_to.get(('transition_of', 'subject'))
         if not eids:
             return []
         return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
@@ -358,7 +358,7 @@
 def state_transitions_vocabulary(form, field):
     entity = form.edited_entity
     if not entity.has_eid():
-        eids = entity.linked_to('state_of', 'subject')
+        eids = form.linked_to.get(('state_of', 'subject'))
         if eids:
             return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
         return []