web/uicfg.py
changeset 3476 6e927b729ae1
parent 3369 7b88d12b4ee2
child 3480 370d20fec445
--- a/web/uicfg.py	Fri Sep 25 11:30:59 2009 +0200
+++ b/web/uicfg.py	Fri Sep 25 11:49:47 2009 +0200
@@ -57,8 +57,44 @@
   simple boolean relation tags used to control the "add entity" submenu.
   Relations whose rtag is True will appears, other won't.
 
+
 Automatic form configuration
 ````````````````````````````
+:autoform_section:
+   where to display a relation in entity form, according to form type.
+   `tag_attribute`, `tag_subject_of` and `tag_object_of` methods for this
+   relation tags expect two arguments additionaly to the relation key: a
+   `formtype` and a `section`.
+
+   formtype may be one of:
+   * 'main', the main entity form
+   * 'inlined', the form for an entity inlined into another's one
+   * 'muledit', the multiple entity (table) form
+
+   section may be one of:
+   * 'hidden', don't display
+   * 'attributes', display in the attributes section
+   * 'relations', display in the relations section, using the generic relation
+     selector combobox (available in main form only, and not for attribute
+     relation)
+   * 'metadata', display in a special metadata form (NOT YET IMPLEMENTED,
+     subject to changes)
+
+:autoform_field:
+  specify a custom field instance to use for a relation
+
+:autoform_field_kwargs:
+
+  specify a dictionnary of arguments to give to the field constructor for a
+  relation. You usually want to use either `autoform_field` or
+  `autoform_field_kwargs`, not both. The later won't have any effect if the
+  former is specified for a relation.
+
+:autoform_permissions_overrides:
+
+  provide a way to by-pass security checking for dark-corner case where it can't
+  be verified properly. XXX documents.
+
 
 :organization: Logilab
 :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -67,9 +103,12 @@
 """
 __docformat__ = "restructuredtext en"
 
+from warnings import warn
+
 from cubicweb import neg_role
-from cubicweb.rtags import (RelationTags, RelationTagsBool,
-                            RelationTagsSet, RelationTagsDict, register_rtag)
+from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
+                            RelationTagsDict, register_rtag, _ensure_str_key)
+from cubicweb.schema import META_RTYPES
 from cubicweb.web import formwidgets
 
 
@@ -144,6 +183,7 @@
 primaryview_display_ctrl = DisplayCtrlRelationTags('primaryview_display_ctrl',
                                                    init_primaryview_display_ctrl)
 
+
 # index view configuration ####################################################
 # entity type section in the index/manage page. May be one of
 # * 'application'
@@ -176,38 +216,182 @@
 
 # autoform.AutomaticEntityForm configuration ##################################
 
-# relations'section (eg primary/secondary/generic/metadata/generated)
+def _formsections_as_dict(formsections):
+    result = {}
+    for formsection in formsections:
+        formtype, section = formsection.split('_', 1)
+        result[formtype] = section
+    return result
+
+def _card_and_comp(sschema, rschema, oschema, role):
+    if role == 'subject':
+        card = rschema.rproperty(sschema, oschema, 'cardinality')[0]
+        composed = rschema.rproperty(sschema, oschema, 'composite') == 'object'
+    else:
+        card = rschema.rproperty(sschema, oschema, 'cardinality')[1]
+        composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject'
+    return card, composed
+
+class AutoformSectionRelationTags(RelationTagsSet):
+    """autoform relations'section"""
 
-def init_autoform_section(rtag, sschema, rschema, oschema, role):
-    if rtag.get(sschema, rschema, oschema, role) is None:
-        if autoform_is_inlined.get(sschema, rschema, oschema, role) or \
-               autoform_is_inlined.get(sschema, rschema, oschema, neg_role(role)):
-            section = 'generated'
-        elif sschema.is_metadata(rschema):
-            section = 'metadata'
-        else:
-            if role == 'subject':
-                card = rschema.rproperty(sschema, oschema, 'cardinality')[0]
-                composed = rschema.rproperty(sschema, oschema, 'composite') == 'object'
+    bw_tag_map = {
+        'primary':   {'main': 'attributes', 'muledit': 'attributes'},
+        'secondary': {'main': 'attributes', 'muledit': 'hidden'},
+        'metadata':  {'main': 'metadata'},
+        'generic':   {'main': 'relations'},
+        'generated': {'main': 'hidden'},
+        }
+
+    _allowed_form_types = ('main', 'inlined', 'muledit')
+    _allowed_values = {'main': ('attributes', 'relations', 'metadata', 'hidden'),
+                       'inlined': ('attributes', 'hidden'),
+                       'muledit': ('attributes', 'hidden'),
+                       }
+
+    @staticmethod
+    def _initfunc(self, sschema, rschema, oschema, role):
+        formsections = self.init_get(sschema, rschema, oschema, role)
+        if formsections is None:
+            formsections = self.tag_container_cls()
+        sectdict = _formsections_as_dict(formsections)
+        if rschema in META_RTYPES:
+            sectdict.setdefault('main', 'hidden')
+            sectdict.setdefault('muledit', 'hidden')
+            sectdict.setdefault('inlined', 'hidden')
+        # ensure we have a tag for each form type
+        if not 'main' in sectdict:
+            if not rschema.is_final() and (
+                sectdict.get('inlined') == 'attributes' or
+                'inlined_attributes' in self.init_get(sschema, rschema, oschema,
+                                                      neg_role(role))):
+                sectdict['main'] = 'hidden'
+            elif sschema.is_metadata(rschema):
+                sectdict['main'] = 'metadata'
             else:
-                card = rschema.rproperty(sschema, oschema, 'cardinality')[1]
-                composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject'
-            if card in '1+':
-                if not rschema.is_final() and composed:
-                    # XXX why? probably because we want it unlined, though this
-                    # is not the case by default
-                    section = 'generated'
+                card, composed = _card_and_comp(sschema, rschema, oschema, role)
+                if card in '1+':
+                    if not rschema.is_final() and composed:
+                        # XXX why? probably because we want it unlined, though
+                        # this is not the case by default
+                        sectdict['main'] = 'hidden'
+                    else:
+                        sectdict['main'] = 'attributes'
+                        if not 'muledit' in sectdict:
+                            sectdict['muledit'] = 'attributes'
+                elif rschema.is_final():
+                    sectdict['main'] = 'attributes'
                 else:
-                    section = 'primary'
-            elif rschema.is_final():
-                section = 'secondary'
-            else:
-                section = 'generic'
-        rtag.tag_relation((sschema, rschema, oschema, role), section)
+                    sectdict['main'] = 'relations'
+        if not 'muledit' in sectdict:
+            sectdict['muledit'] = 'hidden'
+            if sectdict['main'] == 'attributes':
+                card, composed = _card_and_comp(sschema, rschema, oschema, role)
+                if card in '1+' and not composed:
+                    sectdict['muledit'] = 'attributes'
+        if not 'inlined' in sectdict:
+            sectdict['inlined'] = 'hidden'
+        # recompute formsections and set it to avoid recomputing
+        for formtype, section in sectdict.iteritems():
+            formsections.add('%s_%s' % (formtype, section))
+        key = _ensure_str_key( (sschema, rschema, oschema, role) )
+        self._tagdefs[key] = formsections
+
+    def tag_relation(self, key, formtype, section=None):
+        if section is None:
+            tag = formtype
+            for formtype, section in self.bw_tag_map[tag].iteritems():
+                warn('[3.6] add tag to autoform section by specifying form '
+                     'type and tag. Replace %s by formtype=%s, section=%s'
+                     % (tag, formtype, section), DeprecationWarning, stacklevel=2)
+                self.tag_relation(key, formtype, section)
+        assert formtype in self._allowed_form_types, \
+               'formtype should be in (%s), not %s' % (
+            ','.join(self._allowed_form_types), formtype)
+        assert section in self._allowed_values[formtype], \
+               'section for %s should be in (%s), not %s' % (
+            formtype, ','.join(self._allowed_values[formtype]), section)
+        rtags = self._tagdefs.setdefault(_ensure_str_key(key),
+                                         self.tag_container_cls())
+        # remove previous section for this form type if any
+        if rtags:
+            for tag in rtags.copy():
+                if tag.startswith(formtype):
+                    rtags.remove(tag)
+        rtags.add('%s_%s' % (formtype, section))
+        return rtags
+
+    def init_get(self, *key):
+        return super(AutoformSectionRelationTags, self).get(*key)
+
+    def get(self, *key):
+        # overriden to avoid recomputing done in parent classes
+        return self._tagdefs[key]
+
+    def relations_by_section(self, entity, formtype, section,
+                             permission=None, strict=False):
+        """return a list of (relation schema, target schemas, role) for the
+        given entity matching categories and permission.
 
-autoform_section = RelationTags('autoform_section', init_autoform_section,
-                                set(('primary', 'secondary', 'generic',
-                                     'metadata', 'generated')))
+        `strict`:
+          bool telling if having local role is enough (strict = False) or not
+        """
+        tag = '%s_%s' % (formtype, section)
+        eschema  = entity.e_schema
+        permsoverrides = autoform_permissions_overrides
+        if entity.has_eid():
+            eid = entity.eid
+        else:
+            eid = None
+            strict = False
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            # check category first, potentially lower cost than checking
+            # permission which may imply rql queries
+            if tag is not None:
+                targetschemas = [tschema for tschema in targetschemas
+                                 if tag in self.etype_get(eschema, rschema,
+                                                          role, tschema)]
+            if not targetschemas:
+                continue
+            if permission is not None:
+                # tag allowing to hijack the permission machinery when
+                # permission is not verifiable until the entity is actually
+                # created...
+                if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(eschema, rschema, role):
+                    yield (rschema, targetschemas, role)
+                    continue
+                if rschema.is_final():
+                    if not rschema.has_perm(entity._cw, permission, eid):
+                        continue
+                elif role == 'subject':
+                    if not ((not strict and rschema.has_local_role(permission)) or
+                            rschema.has_perm(entity._cw, permission, fromeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
+                        and eid and entity.related(rschema.type, role)
+                        and not rschema.has_perm(entity._cw, 'delete', fromeid=eid,
+                                                 toeid=entity.related(rschema.type, role)[0][0])):
+                        continue
+                elif role == 'object':
+                    if not ((not strict and rschema.has_local_role(permission)) or
+                            rschema.has_perm(entity._cw, permission, toeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
+                        and eid and entity.related(rschema.type, role)
+                        and not rschema.has_perm(entity._cw, 'delete', toeid=eid,
+                                                 fromeid=entity.related(rschema.type, role)[0][0])):
+                        continue
+            yield (rschema, targetschemas, role)
+
+
+
+autoform_section = AutoformSectionRelationTags('autoform_section')
 
 # relations'field class
 autoform_field = RelationTags('autoform_field')
@@ -215,11 +399,6 @@
 # relations'field explicit kwargs (given to field's __init__)
 autoform_field_kwargs = RelationTagsDict()
 
-# inlined view flag for non final relations: when True for an entry, the
-# entity(ies) at the other end of the relation will be editable from the
-# form of the edited entity
-autoform_is_inlined = RelationTagsBool('autoform_is_inlined')
-
 
 # set of tags of the form <action>_on_new on relations. <action> is a
 # schema action (add/update/delete/read), and when such a tag is found
@@ -238,3 +417,20 @@
 
 actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu',
                                                init_actionbox_appearsin_addmenu)
+
+
+# deprecated ###################################################################
+
+class AutoformIsInlined(RelationTags):
+    """XXX for < 3.6 bw compat"""
+    def tag_relation(self, key, tag):
+        warn('autoform_is_inlined rtag is deprecated, use autoform_section '
+             'with inlined formtype and "attributes" or "hidden" section',
+             DeprecationWarning, stacklevel=2)
+        section = tag and 'attributes' or 'hidden'
+        autoform_section.tag_relation(key, 'inlined', section)
+
+# inlined view flag for non final relations: when True for an entry, the
+# entity(ies) at the other end of the relation will be editable from the
+# form of the edited entity
+autoform_is_inlined = AutoformIsInlined('autoform_is_inlined')