refactor vocabulary handling to avoid having to define methods
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 21 Dec 2009 20:00:18 +0100
changeset 4161 4273f5094651
parent 4160 3fbdeef9a610
child 4162 d2663bcf5306
refactor vocabulary handling to avoid having to define methods on form objects, use .choices instead even for non final relations. Details: * deprecated support of <role>_<relation>_vocabulary methods on forms * new utility functions for relations vocabulary * most logic now on the RelationField class
web/box.py
web/formfields.py
web/views/editviews.py
web/views/forms.py
web/views/massmailing.py
--- a/web/box.py	Mon Dec 21 19:52:21 2009 +0100
+++ b/web/box.py	Mon Dec 21 20:00:18 2009 +0100
@@ -221,7 +221,7 @@
         form = self.vreg['forms'].select('edition', self._cw, rset=self.cw_rset,
                                          row=self.cw_row or 0)
         field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
-        for _, eid in form.form_field_vocabulary(field):
+        for _, eid in field.choices(form):
             if eid is not None:
                 rset = self._cw.eid_rset(eid)
                 entities.append(rset.get_entity(0, 0))
--- a/web/formfields.py	Mon Dec 21 19:52:21 2009 +0100
+++ b/web/formfields.py	Mon Dec 21 20:00:18 2009 +0100
@@ -277,19 +277,16 @@
 
     def vocabulary(self, form):
         """return vocabulary for this field. This method will be called by
-        widgets which desire it."""
-        if self.choices is not None:
-            if callable(self.choices):
-                try:
-                    vocab = self.choices(form=form)
-                except TypeError:
-                    warn('[3.3] vocabulary method (eg field.choices) should now take '
-                         'the form instance as argument', DeprecationWarning)
-                    vocab = self.choices(req=form._cw)
-            else:
-                vocab = self.choices
-            if vocab and not isinstance(vocab[0], (list, tuple)):
-                vocab = [(x, x) for x in vocab]
+        widgets which requires a vocabulary.
+        """
+        assert self.choices is not None
+        if callable(self.choices):
+            try:
+                vocab = self.choices(form=form)
+            except TypeError:
+                warn('[3.3] vocabulary method (eg field.choices) should now take '
+                     'the form instance as argument', DeprecationWarning)
+                vocab = self.choices(req=form._cw)
         else:
             vocab = form.form_field_vocabulary(self)
         if self.internationalizable:
@@ -644,6 +641,60 @@
             time = form.parse_time(wdgdate, 'Time')
         return time
 
+
+# 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'), 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'), 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'), entity.eid))
+    return res
+
+
 class RelationField(Field):
 
     @staticmethod
@@ -651,24 +702,21 @@
         kwargs.setdefault('widget', Select(multiple=card in '*+'))
         return RelationField(**kwargs)
 
-    def vocabulary(self, form):
+    def choices(self, form, limit=None):
         entity = form.edited_entity
         # first see if its specified by __linkto form parameters
-        linkedto = entity.linked_to(self.name, self.role)
+        linkedto = relvoc_linkedto(entity, self.name, self.role)
         if linkedto:
-            entities = (req.entity_from_eid(eid) for eid in linkedto)
-            return [(entity.view('combobox'), entity.eid) for entity in entities]
+            return linkedto
         # it isn't, check if the entity provides a method to get correct values
-        res = []
-        if not self.required:
-            res.append(('', INTERNAL_FIELD_VALUE))
-        # vocabulary doesn't include current values, add them
-        if entity.has_eid():
-            rset = entity.related(self.name, self.role)
-            relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
-        else:
-            relatedvocab = []
-        vocab = res + form.form_field_vocabulary(self) + relatedvocab
+        vocab = relvoc_init(entity, self.name, self.role, self.required)
+        method = '%s_%s_vocabulary' % (self.role, self.name)
+        try:
+            vocab += getattr(form, method)(rtype, limit)
+            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)
         if self.sort:
             vocab = vocab_sort(vocab)
         return vocab
--- a/web/views/editviews.py	Mon Dec 21 19:52:21 2009 +0100
+++ b/web/views/editviews.py	Mon Dec 21 20:00:18 2009 +0100
@@ -117,18 +117,17 @@
     def _get_select_options(self, entity, rschema, target):
         """add options to search among all entities of each possible type"""
         options = []
-        eid = entity.eid
-        pending_inserts = self._cw.get_pending_inserts(eid)
+        pending_inserts = self._cw.get_pending_inserts(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)
         limit = self._cw.property_value('navigation.combobox-limit')
-        for eview, reid in form.form_field_vocabulary(field, 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))
             else:
-                optionid = relation_id(eid, rtype, target, reid)
+                optionid = relation_id(entity.eid, rtype, target, 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>' %
--- a/web/views/forms.py	Mon Dec 21 19:52:21 2009 +0100
+++ b/web/views/forms.py	Mon Dec 21 20:00:18 2009 +0100
@@ -17,7 +17,7 @@
 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
 from cubicweb.web import form, formwidgets as fwdgs
 from cubicweb.web.controller import NAV_FORM_PARAMETERS
-from cubicweb.web.formfields import StringField
+from cubicweb.web.formfields import StringField, relvoc_unrelated
 
 
 class FieldsForm(form.Form):
@@ -174,12 +174,6 @@
         """return encoding used for the given (text) field"""
         return self._cw.encoding
 
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field. Should be overriden in
-        specific forms using fields which requires some vocabulary
-        """
-        raise NotImplementedError
-
     def form_field_modified(self, field):
         return field.is_visible()
 
@@ -257,52 +251,6 @@
         return super(EntityFieldsForm, self)._field_has_error(field) \
                and self.form_valerror.eid == self.edited_entity.eid
 
-    def _relation_vocabulary(self, rtype, targettype, role,
-                            limit=None, done=None):
-        """return unrelated entities for a given relation and target entity type
-        for use in vocabulary
-        """
-        if done is None:
-            done = set()
-        rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
-        res = []
-        for entity in rset.entities():
-            if entity.eid in done:
-                continue
-            done.add(entity.eid)
-            res.append((entity.view('combobox'), entity.eid))
-        return res
-
-    def _req_display_value(self, field):
-        value = super(EntityFieldsForm, self)._req_display_value(field)
-        if value is None:
-            value = self.edited_entity.linked_to(field.name, field.role)
-            if value:
-                searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
-                                  for eid in value]
-                # remove associated __linkto hidden fields
-                for field in self.root_form.fields_by_name('__linkto'):
-                    if field.initial in searchedvalues:
-                        self.root_form.remove_field(field)
-            else:
-                value = None
-        return value
-
-    def _form_field_default_value(self, field, load_bytes):
-        defaultattr = 'default_%s' % field.name
-        if hasattr(self.edited_entity, defaultattr):
-            # XXX bw compat, default_<field name> on the entity
-            warn('found %s on %s, should be set on a specific form'
-                 % (defaultattr, self.edited_entity.__regid__), DeprecationWarning)
-            value = getattr(self.edited_entity, defaultattr)
-            if callable(value):
-                value = value()
-        else:
-            value = super(EntityFieldsForm, self).form_field_value(field,
-                                                                   load_bytes)
-        return value
-
-    def form_default_renderer(self):
         return self._cw.vreg['formrenderers'].select(
             self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row,
             col=self.cw_col, entity=self.edited_entity)
@@ -325,34 +273,14 @@
             return self.edited_entity.attr_metadata(field.name, 'encoding')
         return super(EntityFieldsForm, self).form_field_encoding(field)
     # XXX all this vocabulary handling should be on the field, no?
-
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field"""
-        role, rtype = field.role, field.name
-        method = '%s_%s_vocabulary' % (role, rtype)
     def actual_eid(self, eid):
         # should be either an int (existant entity) or a variable (to be
         # created entity)
         assert eid or eid == 0, repr(eid) # 0 is a valid eid
         try:
-            vocabfunc = getattr(self, method)
-        except AttributeError:
             return typed_eid(eid)
         except ValueError:
             try:
-                # XXX bw compat, <role>_<rtype>_vocabulary on the entity
-                vocabfunc = getattr(self.edited_entity, method)
-            except AttributeError:
-                vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
-            else:
-                warn('found %s on %s, should be set on a specific form'
-                     % (method, self.edited_entity.__regid__), DeprecationWarning)
-        # NOTE: it is the responsibility of `vocabfunc` to sort the result
-        #       (direclty through RQL or via a python sort). This is also
-        #       important because `vocabfunc` might return a list with
-        #       couples (label, None) which act as separators. In these
-        #       cases, it doesn't make sense to sort results afterwards.
-        return vocabfunc(rtype, limit)
 
     def form_field_modified(self, field):
         if field.is_visible():
@@ -379,49 +307,6 @@
                 return False # not modified
             return True
         return False
-
-    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)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = self._cw.vreg.schema.rschema(rtype)
-        done = None
-        assert not rtype.final, rtype
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, str(rtype)))
-        result = []
-        rsetsize = None
-        for objtype in rtype.objects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, objtype, 'subject',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def object_relation_vocabulary(self, rtype, limit=None):
-        """defaut vocabulary method for the given relation, looking for
-        relation's subject entities (i.e. self is the object)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = self._cw.vreg.schema.rschema(rtype)
-        done = None
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
-        result = []
-        rsetsize = None
-        for subjtype in rtype.subjects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, subjtype, 'object',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
                 return self._cw.data['eidmap'][eid]
             except KeyError:
                 self._cw.data['eidmap'][eid] = None
@@ -433,6 +318,17 @@
     def should_display_add_new_relation_link(self, rschema, existant, card):
         return False
 
+    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
+    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)
+
+    @deprecated('[3.6] use cw.web.formfields.relvoc_unrelated function')
+    def object_relation_vocabulary(self, rtype, limit=None):
+        return relvoc_unrelated(self.edited_entity, rtype, 'object', limit=None)
+
 
 class CompositeFormMixIn(object):
     """form composed of sub-forms"""
--- a/web/views/massmailing.py	Mon Dec 21 19:52:21 2009 +0100
+++ b/web/views/massmailing.py	Mon Dec 21 20:00:18 2009 +0100
@@ -35,6 +35,10 @@
                                   **params)
 
 
+def recipient_vocabulary(form):
+    vocab = [(entity.get_email(), entity.eid) for entity in form.cw_rset.entities()]
+    return [(label, value) for label, value in vocab if label]
+
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
@@ -54,12 +58,6 @@
                               stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
     form_renderer_id = __regid__
 
-    def form_field_vocabulary(self, field):
-        if field.name == 'recipient':
-            vocab = [(entity.get_email(), entity.eid) for entity in self.cw_rset.entities()]
-            return [(label, value) for label, value in vocab if label]
-        return super(MassMailingForm, self).form_field_vocabulary(field)
-
     def __init__(self, *args, **kwargs):
         super(MassMailingForm, self).__init__(*args, **kwargs)
         field = self.field_by_name('mailbody')