web/views/uicfg.py
changeset 8665 e65af61bde7d
child 8666 1dd655788ece
equal deleted inserted replaced
8664:29652410c317 8665:e65af61bde7d
       
     1 # copyright 2003-2011 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 """This module (``cubicweb.web.uicfg``) regroups a set of structures that may be
       
    19 used to configure various options of the generated web interface.
       
    20 
       
    21 To configure the interface generation, we use ``RelationTag`` objects.
       
    22 
       
    23 Index view configuration
       
    24 ````````````````````````
       
    25 :indexview_etype_section:
       
    26    entity type category in the index/manage page. May be one of:
       
    27 
       
    28       * ``application``
       
    29       * ``system``
       
    30       * ``schema``
       
    31       * ``subobject`` (not displayed by default)
       
    32 
       
    33    By default only entities on the ``application`` category are shown.
       
    34 
       
    35 .. sourcecode:: python
       
    36 
       
    37     from cubicweb.web import uicfg
       
    38     # force hiding
       
    39     uicfg.indexview_etype_section['HideMe'] = 'subobject'
       
    40     # force display
       
    41     uicfg.indexview_etype_section['ShowMe'] = 'application'
       
    42 
       
    43 
       
    44 Actions box configuration
       
    45 `````````````````````````
       
    46 :actionbox_appearsin_addmenu:
       
    47   simple boolean relation tags used to control the "add entity" submenu.
       
    48   Relations whose rtag is True will appears, other won't.
       
    49 
       
    50 .. sourcecode:: python
       
    51 
       
    52    # Adds all subjects of the entry_of relation in the add menu of the ``Blog``
       
    53    # primary view
       
    54    uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
       
    55 """
       
    56 __docformat__ = "restructuredtext en"
       
    57 
       
    58 from warnings import warn
       
    59 
       
    60 from logilab.common.compat import any
       
    61 
       
    62 from cubicweb import neg_role
       
    63 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
       
    64                             RelationTagsDict, NoTargetRelationTagsDict,
       
    65                             register_rtag, _ensure_str_key)
       
    66 from cubicweb.schema import META_RTYPES, INTERNAL_TYPES, WORKFLOW_TYPES
       
    67 
       
    68 
       
    69 # primary view configuration ##################################################
       
    70 
       
    71 def init_primaryview_section(rtag, sschema, rschema, oschema, role):
       
    72     if rtag.get(sschema, rschema, oschema, role) is None:
       
    73         rdef = rschema.rdef(sschema, oschema)
       
    74         if rschema.final:
       
    75             if rschema.meta or sschema.is_metadata(rschema) \
       
    76                     or oschema.type in ('Password', 'Bytes'):
       
    77                 section = 'hidden'
       
    78             else:
       
    79                 section = 'attributes'
       
    80         else:
       
    81             if rdef.role_cardinality(role) in '1+':
       
    82                 section = 'attributes'
       
    83             elif rdef.composite == neg_role(role):
       
    84                 section = 'relations'
       
    85             else:
       
    86                 section = 'sideboxes'
       
    87         rtag.tag_relation((sschema, rschema, oschema, role), section)
       
    88 
       
    89 primaryview_section = RelationTags('primaryview_section',
       
    90                                    init_primaryview_section,
       
    91                                    frozenset(('attributes', 'relations',
       
    92                                               'sideboxes', 'hidden')))
       
    93 
       
    94 
       
    95 class DisplayCtrlRelationTags(NoTargetRelationTagsDict):
       
    96     def __init__(self, *args, **kwargs):
       
    97         super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs)
       
    98         self.counter = 0
       
    99 
       
   100 def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role):
       
   101     if role == 'subject':
       
   102         oschema = '*'
       
   103     else:
       
   104         sschema = '*'
       
   105     rtag.counter += 1
       
   106     rtag.setdefault((sschema, rschema, oschema, role), 'order', rtag.counter)
       
   107 
       
   108 primaryview_display_ctrl = DisplayCtrlRelationTags('primaryview_display_ctrl',
       
   109                                                    init_primaryview_display_ctrl)
       
   110 
       
   111 
       
   112 # index view configuration ####################################################
       
   113 # entity type section in the index/manage page. May be one of
       
   114 # * 'application'
       
   115 # * 'system'
       
   116 # * 'schema'
       
   117 # * 'hidden'
       
   118 # * 'subobject' (not displayed by default)
       
   119 
       
   120 class InitializableDict(dict):
       
   121     def __init__(self, *args, **kwargs):
       
   122         super(InitializableDict, self).__init__(*args, **kwargs)
       
   123         register_rtag(self)
       
   124         self.__defaults = dict(self)
       
   125 
       
   126     def init(self, schema, check=True):
       
   127         self.update(self.__defaults)
       
   128         for eschema in schema.entities():
       
   129             if eschema.final:
       
   130                 continue
       
   131             if eschema.schema_entity():
       
   132                 self.setdefault(eschema, 'schema')
       
   133             elif eschema in INTERNAL_TYPES or eschema in WORKFLOW_TYPES:
       
   134                 self.setdefault(eschema, 'system')
       
   135             elif eschema.is_subobject(strict=True):
       
   136                 self.setdefault(eschema, 'subobject')
       
   137             else:
       
   138                 self.setdefault(eschema, 'application')
       
   139 
       
   140 indexview_etype_section = InitializableDict(
       
   141     EmailAddress='subobject',
       
   142     Bookmark='system',
       
   143     # entity types in the 'system' table by default (managers only)
       
   144     CWUser='system', CWGroup='system',
       
   145     )
       
   146 
       
   147 # autoform.AutomaticEntityForm configuration ##################################
       
   148 
       
   149 def _formsections_as_dict(formsections):
       
   150     result = {}
       
   151     for formsection in formsections:
       
   152         formtype, section = formsection.split('_', 1)
       
   153         result[formtype] = section
       
   154     return result
       
   155 
       
   156 def _card_and_comp(sschema, rschema, oschema, role):
       
   157     rdef = rschema.rdef(sschema, oschema)
       
   158     if role == 'subject':
       
   159         card = rdef.cardinality[0]
       
   160         composed = not rschema.final and rdef.composite == 'object'
       
   161     else:
       
   162         card = rdef.cardinality[1]
       
   163         composed = not rschema.final and rdef.composite == 'subject'
       
   164     return card, composed
       
   165 
       
   166 class AutoformSectionRelationTags(RelationTagsSet):
       
   167     """autoform relations'section"""
       
   168 
       
   169     _allowed_form_types = ('main', 'inlined', 'muledit')
       
   170     _allowed_values = {'main': ('attributes', 'inlined', 'relations',
       
   171                                 'metadata', 'hidden'),
       
   172                        'inlined': ('attributes', 'inlined', 'hidden'),
       
   173                        'muledit': ('attributes', 'hidden'),
       
   174                        }
       
   175 
       
   176     def init(self, schema, check=True):
       
   177         super(AutoformSectionRelationTags, self).init(schema, check)
       
   178         self.apply(schema, self._initfunc_step2)
       
   179 
       
   180     @staticmethod
       
   181     def _initfunc(self, sschema, rschema, oschema, role):
       
   182         formsections = self.init_get(sschema, rschema, oschema, role)
       
   183         if formsections is None:
       
   184             formsections = self.tag_container_cls()
       
   185         if not any(tag.startswith('inlined') for tag in formsections):
       
   186             if not rschema.final:
       
   187                 negsects = self.init_get(sschema, rschema, oschema, neg_role(role))
       
   188                 if 'main_inlined' in negsects:
       
   189                     formsections.add('inlined_hidden')
       
   190         key = _ensure_str_key( (sschema, rschema, oschema, role) )
       
   191         self._tagdefs[key] = formsections
       
   192 
       
   193     @staticmethod
       
   194     def _initfunc_step2(self, sschema, rschema, oschema, role):
       
   195         formsections = self.get(sschema, rschema, oschema, role)
       
   196         sectdict = _formsections_as_dict(formsections)
       
   197         if rschema in META_RTYPES:
       
   198             sectdict.setdefault('main', 'hidden')
       
   199             sectdict.setdefault('muledit', 'hidden')
       
   200             sectdict.setdefault('inlined', 'hidden')
       
   201         elif role == 'subject' and rschema in sschema.meta_attributes():
       
   202             # meta attribute, usually embeded by the described attribute's field
       
   203             # (eg RichTextField, FileField...)
       
   204             sectdict.setdefault('main', 'hidden')
       
   205             sectdict.setdefault('muledit', 'hidden')
       
   206             sectdict.setdefault('inlined', 'hidden')
       
   207         # ensure we have a tag for each form type
       
   208         if not 'main' in sectdict:
       
   209             if not rschema.final and (
       
   210                 sectdict.get('inlined') == 'attributes' or
       
   211                 'inlined_attributes' in self.init_get(sschema, rschema, oschema,
       
   212                                                       neg_role(role))):
       
   213                 sectdict['main'] = 'hidden'
       
   214             elif sschema.is_metadata(rschema):
       
   215                 sectdict['main'] = 'metadata'
       
   216             else:
       
   217                 card, composed = _card_and_comp(sschema, rschema, oschema, role)
       
   218                 if card in '1+':
       
   219                     sectdict['main'] = 'attributes'
       
   220                     if not 'muledit' in sectdict:
       
   221                         sectdict['muledit'] = 'attributes'
       
   222                 elif rschema.final:
       
   223                     sectdict['main'] = 'attributes'
       
   224                 else:
       
   225                     sectdict['main'] = 'relations'
       
   226         if not 'muledit' in sectdict:
       
   227             sectdict['muledit'] = 'hidden'
       
   228             if sectdict['main'] == 'attributes':
       
   229                 card, composed = _card_and_comp(sschema, rschema, oschema, role)
       
   230                 if card in '1+' and not composed:
       
   231                     sectdict['muledit'] = 'attributes'
       
   232         if not 'inlined' in sectdict:
       
   233             sectdict['inlined'] = sectdict['main']
       
   234         # recompute formsections and set it to avoid recomputing
       
   235         for formtype, section in sectdict.iteritems():
       
   236             formsections.add('%s_%s' % (formtype, section))
       
   237 
       
   238     def tag_relation(self, key, formtype, section):
       
   239         if isinstance(formtype, tuple):
       
   240             for ftype in formtype:
       
   241                 self.tag_relation(key, ftype, section)
       
   242             return
       
   243         assert formtype in self._allowed_form_types, \
       
   244                'formtype should be in (%s), not %s' % (
       
   245             ','.join(self._allowed_form_types), formtype)
       
   246         assert section in self._allowed_values[formtype], \
       
   247                'section for %s should be in (%s), not %s' % (
       
   248             formtype, ','.join(self._allowed_values[formtype]), section)
       
   249         rtags = self._tagdefs.setdefault(_ensure_str_key(key),
       
   250                                          self.tag_container_cls())
       
   251         # remove previous section for this form type if any
       
   252         if rtags:
       
   253             for tag in rtags.copy():
       
   254                 if tag.startswith(formtype):
       
   255                     rtags.remove(tag)
       
   256         rtags.add('%s_%s' % (formtype, section))
       
   257         return rtags
       
   258 
       
   259     def init_get(self, stype, rtype, otype, tagged):
       
   260         key = (stype, rtype, otype, tagged)
       
   261         rtags = {}
       
   262         for key in self._get_keys(stype, rtype, otype, tagged):
       
   263             tags = self._tagdefs.get(key, ())
       
   264             for tag in tags:
       
   265                 assert '_' in tag, (tag, tags)
       
   266                 section, value = tag.split('_', 1)
       
   267                 rtags[section] = value
       
   268         cls = self.tag_container_cls
       
   269         rtags = cls('_'.join([section,value]) for section,value in rtags.iteritems())
       
   270         return rtags
       
   271 
       
   272 
       
   273     def get(self, *key):
       
   274         # overriden to avoid recomputing done in parent classes
       
   275         return self._tagdefs.get(key, ())
       
   276 
       
   277     def relations_by_section(self, entity, formtype, section, permission,
       
   278                              strict=False):
       
   279         """return a list of (relation schema, target schemas, role) for the
       
   280         given entity matching categories and permission.
       
   281 
       
   282         `strict`:
       
   283           bool telling if having local role is enough (strict = False) or not
       
   284         """
       
   285         tag = '%s_%s' % (formtype, section)
       
   286         eschema  = entity.e_schema
       
   287         permsoverrides = autoform_permissions_overrides
       
   288         if entity.has_eid():
       
   289             eid = entity.eid
       
   290         else:
       
   291             eid = None
       
   292             strict = False
       
   293         if permission == 'update':
       
   294             assert section in ('attributes', 'metadata', 'hidden')
       
   295             relpermission = 'add'
       
   296         else:
       
   297             assert section not in ('attributes', 'metadata', 'hidden')
       
   298             relpermission = permission
       
   299         cw = entity._cw
       
   300         for rschema, targetschemas, role in eschema.relation_definitions(True):
       
   301             _targetschemas = []
       
   302             for tschema in targetschemas:
       
   303                 # check section's tag first, potentially lower cost than
       
   304                 # checking permission which may imply rql queries
       
   305                 if not tag in self.etype_get(eschema, rschema, role, tschema):
       
   306                     continue
       
   307                 rdef = rschema.role_rdef(eschema, tschema, role)
       
   308                 if rschema.final:
       
   309                     if not rdef.has_perm(cw, permission, eid=eid,
       
   310                                          creating=eid is None):
       
   311                         continue
       
   312                 elif strict or not rdef.has_local_role(relpermission):
       
   313                     if role == 'subject':
       
   314                         if not rdef.has_perm(cw, relpermission, fromeid=eid):
       
   315                             continue
       
   316                     elif role == 'object':
       
   317                         if not rdef.has_perm(cw, relpermission, toeid=eid):
       
   318                             continue
       
   319                 _targetschemas.append(tschema)
       
   320             if not _targetschemas:
       
   321                 continue
       
   322             targetschemas = _targetschemas
       
   323             rdef = eschema.rdef(rschema, role=role, targettype=targetschemas[0])
       
   324             # XXX tag allowing to hijack the permission machinery when
       
   325             # permission is not verifiable until the entity is actually
       
   326             # created...
       
   327             if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(eschema, rschema, role):
       
   328                 yield (rschema, targetschemas, role)
       
   329                 continue
       
   330             if not rschema.final and role == 'subject':
       
   331                 # on relation with cardinality 1 or ?, we need delete perm as well
       
   332                 # if the relation is already set
       
   333                 if (relpermission == 'add'
       
   334                     and rdef.role_cardinality(role) in '1?'
       
   335                     and eid and entity.related(rschema.type, role)
       
   336                     and not rdef.has_perm(cw, 'delete', fromeid=eid,
       
   337                                           toeid=entity.related(rschema.type, role)[0][0])):
       
   338                     continue
       
   339             elif role == 'object':
       
   340                 # on relation with cardinality 1 or ?, we need delete perm as well
       
   341                 # if the relation is already set
       
   342                 if (relpermission == 'add'
       
   343                     and rdef.role_cardinality(role) in '1?'
       
   344                     and eid and entity.related(rschema.type, role)
       
   345                     and not rdef.has_perm(cw, 'delete', toeid=eid,
       
   346                                           fromeid=entity.related(rschema.type, role)[0][0])):
       
   347                     continue
       
   348             yield (rschema, targetschemas, role)
       
   349 
       
   350 autoform_section = AutoformSectionRelationTags('autoform_section')
       
   351 
       
   352 # relations'field class
       
   353 autoform_field = RelationTags('autoform_field')
       
   354 
       
   355 # relations'field explicit kwargs (given to field's __init__)
       
   356 autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs')
       
   357 
       
   358 
       
   359 # set of tags of the form <action>_on_new on relations. <action> is a
       
   360 # schema action (add/update/delete/read), and when such a tag is found
       
   361 # permissions checking is by-passed and supposed to be ok
       
   362 autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
       
   363 
       
   364 class ReleditTags(NoTargetRelationTagsDict):
       
   365     """Associate to relation a dictionary to control `reledit` (e.g. edition of
       
   366     attributes / relations from within views).
       
   367 
       
   368     Possible keys and associated values are:
       
   369 
       
   370     * `novalue_label`, alternative default value (shown when there is no value).
       
   371 
       
   372     * `novalue_include_rtype`, when `novalue_label` is not specified, this boolean
       
   373       flag control wether the generated default value should contains the
       
   374       relation label or not. Will be the opposite of the `showlabel` value found
       
   375       in the `primaryview_display_ctrl` rtag by default.
       
   376 
       
   377     * `reload`, boolean, eid (to reload to) or function taking subject and
       
   378       returning bool/eid. This is useful when editing a relation (or attribute)
       
   379       that impacts the url or another parts of the current displayed
       
   380       page. Defaults to False.
       
   381 
       
   382     * `rvid`, alternative view id (as str) for relation or composite edition.
       
   383       Default is 'autolimited'.
       
   384 
       
   385     * `edit_target`, may be either 'rtype' (to edit the relation) or 'related'
       
   386       (to edit the related entity).  This controls whether to edit the relation
       
   387       or the target entity of the relation.  Currently only one-to-one relations
       
   388       support target entity edition. By default, the 'related' option is taken
       
   389       whenever the relation is composite.
       
   390     """
       
   391     _keys = frozenset('novalue_label novalue_include_rtype reload rvid edit_target'.split())
       
   392 
       
   393     def tag_relation(self, key, tag):
       
   394         for tagkey in tag.iterkeys():
       
   395             assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys)
       
   396         return super(ReleditTags, self).tag_relation(key, tag)
       
   397 
       
   398 def init_reledit_ctrl(rtag, sschema, rschema, oschema, role):
       
   399     values = rtag.get(sschema, rschema, oschema, role)
       
   400     if not rschema.final:
       
   401         composite = rschema.rdef(sschema, oschema).composite == role
       
   402         if role == 'subject':
       
   403             oschema = '*'
       
   404         else:
       
   405             sschema = '*'
       
   406         edittarget = values.get('edit_target')
       
   407         if edittarget not in (None, 'rtype', 'related'):
       
   408             rtag.warning('reledit: wrong value for edit_target on relation %s: %s',
       
   409                          rschema, edittarget)
       
   410             edittarget = None
       
   411         if not edittarget:
       
   412             edittarget = 'related' if composite else 'rtype'
       
   413             rtag.tag_relation((sschema, rschema, oschema, role),
       
   414                               {'edit_target': edittarget})
       
   415     if not 'novalue_include_rtype' in values:
       
   416         showlabel = primaryview_display_ctrl.get(
       
   417             sschema, rschema, oschema, role).get('showlabel', True)
       
   418         rtag.tag_relation((sschema, rschema, oschema, role),
       
   419                           {'novalue_include_rtype': not showlabel})
       
   420 
       
   421 reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl)
       
   422 
       
   423 # boxes.EditBox configuration #################################################
       
   424 
       
   425 # 'link' / 'create' relation tags, used to control the "add entity" submenu
       
   426 def init_actionbox_appearsin_addmenu(rtag, sschema, rschema, oschema, role):
       
   427     if rtag.get(sschema, rschema, oschema, role) is None:
       
   428         if rschema in META_RTYPES:
       
   429             rtag.tag_relation((sschema, rschema, oschema, role), False)
       
   430             return
       
   431         rdef = rschema.rdef(sschema, oschema)
       
   432         if not rdef.role_cardinality(role) in '?1' and rdef.composite == role:
       
   433             rtag.tag_relation((sschema, rschema, oschema, role), True)
       
   434 
       
   435 actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu',
       
   436                                                init_actionbox_appearsin_addmenu)