web/views/autoform.py
branchtls-sprint
changeset 1491 742aff97dbf7
child 1498 2c6eec0b46b9
equal deleted inserted replaced
1486:12bba5e13cf9 1491:742aff97dbf7
       
     1 """The automatic entity form.
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 from logilab.common.decorators import iclassmethod
       
    10 
       
    11 from cubicweb import typed_eid
       
    12 from cubicweb.web import stdmsgs, uicfg
       
    13 from cubicweb.web.form import FieldNotFound, EntityFieldsForm
       
    14 from cubicweb.web.formwidgets import Button, SubmitButton
       
    15 _ = unicode
       
    16 
       
    17 class AutomaticEntityForm(EntityFieldsForm):
       
    18     """base automatic form to edit any entity.
       
    19 
       
    20     Designed to be flly generated from schema but highly configurable through:
       
    21     * rtags (rcategories, rfields, rwidgets, inlined, rpermissions)
       
    22     * various standard form parameters
       
    23 
       
    24     You can also easily customise it by adding/removing fields in
       
    25     AutomaticEntityForm instances.
       
    26     """
       
    27     id = 'edition'
       
    28 
       
    29     cwtarget = 'eformframe'
       
    30     cssclass = 'entityForm'
       
    31     copy_nav_params = True
       
    32     form_buttons = [SubmitButton(stdmsgs.BUTTON_OK),
       
    33                     Button(stdmsgs.BUTTON_APPLY, cwaction='apply'),
       
    34                     Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
       
    35     attrcategories = ('primary', 'secondary')
       
    36     # class attributes below are actually stored in the uicfg module since we
       
    37     # don't want them to be reloaded
       
    38     rcategories = uicfg.rcategories
       
    39     rfields = uicfg.rfields
       
    40     rwidgets = uicfg.rwidgets
       
    41     rinlined = uicfg.rinlined
       
    42     rpermissions_overrides = uicfg.rpermissions_overrides
       
    43 
       
    44     @classmethod
       
    45     def vreg_initialization_completed(cls):
       
    46         """set default category tags for relations where it's not yet defined in
       
    47         the category relation tags
       
    48         """
       
    49         for eschema in cls.schema.entities():
       
    50             for rschema, tschemas, role in eschema.relation_definitions(True):
       
    51                 for tschema in tschemas:
       
    52                     if role == 'subject':
       
    53                         X, Y = eschema, tschema
       
    54                         card = rschema.rproperty(X, Y, 'cardinality')[0]
       
    55                         composed = rschema.rproperty(X, Y, 'composite') == 'object'
       
    56                     else:
       
    57                         X, Y = tschema, eschema
       
    58                         card = rschema.rproperty(X, Y, 'cardinality')[1]
       
    59                         composed = rschema.rproperty(X, Y, 'composite') == 'subject'
       
    60                     if not cls.rcategories.rtag(rschema, role, X, Y):
       
    61                         if card in '1+':
       
    62                             if not rschema.is_final() and composed:
       
    63                                 category = 'generated'
       
    64                             else:
       
    65                                 category = 'primary'
       
    66                         elif rschema.is_final():
       
    67                             category = 'secondary'
       
    68                         else:
       
    69                             category = 'generic'
       
    70                         cls.rcategories.set_rtag(category, rschema, role, X, Y)
       
    71 
       
    72     @classmethod
       
    73     def erelations_by_category(cls, entity, categories=None, permission=None, rtags=None):
       
    74         """return a list of (relation schema, target schemas, role) matching
       
    75         categories and permission
       
    76         """
       
    77         if categories is not None:
       
    78             if not isinstance(categories, (list, tuple, set, frozenset)):
       
    79                 categories = (categories,)
       
    80             if not isinstance(categories, (set, frozenset)):
       
    81                 categories = frozenset(categories)
       
    82         eschema  = entity.e_schema
       
    83         if rtags is None:
       
    84             rtags = cls.rcategories
       
    85         permsoverrides = cls.rpermissions_overrides
       
    86         if entity.has_eid():
       
    87             eid = entity.eid
       
    88         else:
       
    89             eid = None
       
    90         for rschema, targetschemas, role in eschema.relation_definitions(True):
       
    91             # check category first, potentially lower cost than checking
       
    92             # permission which may imply rql queries
       
    93             if categories is not None:
       
    94                 targetschemas = [tschema for tschema in targetschemas
       
    95                                  if rtags.etype_rtag(eschema, rschema, role, tschema) in categories]
       
    96                 if not targetschemas:
       
    97                     continue
       
    98             if permission is not None:
       
    99                 # tag allowing to hijack the permission machinery when
       
   100                 # permission is not verifiable until the entity is actually
       
   101                 # created...
       
   102                 if eid is None and '%s_on_new' % permission in permsoverrides.etype_rtags(eschema, rschema, role):
       
   103                     yield (rschema, targetschemas, role)
       
   104                     continue
       
   105                 if rschema.is_final():
       
   106                     if not rschema.has_perm(entity.req, permission, eid):
       
   107                         continue
       
   108                 elif role == 'subject':
       
   109                     if not ((eid is None and rschema.has_local_role(permission)) or
       
   110                             rschema.has_perm(entity.req, permission, fromeid=eid)):
       
   111                         continue
       
   112                     # on relation with cardinality 1 or ?, we need delete perm as well
       
   113                     # if the relation is already set
       
   114                     if (permission == 'add'
       
   115                         and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
       
   116                         and eid and entity.related(rschema.type, role)
       
   117                         and not rschema.has_perm(entity.req, 'delete', fromeid=eid,
       
   118                                                  toeid=entity.related(rschema.type, role)[0][0])):
       
   119                         continue
       
   120                 elif role == 'object':
       
   121                     if not ((eid is None and rschema.has_local_role(permission)) or
       
   122                             rschema.has_perm(entity.req, permission, toeid=eid)):
       
   123                         continue
       
   124                     # on relation with cardinality 1 or ?, we need delete perm as well
       
   125                     # if the relation is already set
       
   126                     if (permission == 'add'
       
   127                         and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
       
   128                         and eid and entity.related(rschema.type, role)
       
   129                         and not rschema.has_perm(entity.req, 'delete', toeid=eid,
       
   130                                                  fromeid=entity.related(rschema.type, role)[0][0])):
       
   131                         continue
       
   132             yield (rschema, targetschemas, role)
       
   133 
       
   134     @classmethod
       
   135     def esrelations_by_category(cls, entity, categories=None, permission=None):
       
   136         """filter out result of relations_by_category(categories, permission) by
       
   137         removing final relations
       
   138 
       
   139         return a sorted list of (relation's label, relation'schema, role)
       
   140         """
       
   141         result = []
       
   142         for rschema, ttypes, role in cls.erelations_by_category(
       
   143             entity, categories, permission):
       
   144             if rschema.is_final():
       
   145                 continue
       
   146             result.append((rschema.display_name(entity.req, role), rschema, role))
       
   147         return sorted(result)
       
   148 
       
   149     @iclassmethod
       
   150     def field_by_name(cls_or_self, name, role='subject', eschema=None):
       
   151         """return field with the given name and role. If field is not explicitly
       
   152         defined for the form but `eclass` is specified, guess_field will be
       
   153         called.
       
   154         """
       
   155         try:
       
   156             return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role)
       
   157         except FieldNotFound: # XXX should raise more explicit exception
       
   158             if eschema is None or not name in cls_or_self.schema:
       
   159                 raise
       
   160             rschema = cls_or_self.schema.rschema(name)
       
   161             fieldcls = cls_or_self.rfields.etype_rtag(eschema, rschema, role)
       
   162             if fieldcls:
       
   163                 return fieldcls(name=name, role=role, eidparam=True)
       
   164             widget = cls_or_self.rwidgets.etype_rtag(eschema, rschema, role)
       
   165             if widget:
       
   166                 field = guess_field(eschema, rschema, role,
       
   167                                     eidparam=True, widget=widget)
       
   168             else:
       
   169                 field = guess_field(eschema, rschema, role, eidparam=True)
       
   170             if field is None:
       
   171                 raise
       
   172             return field
       
   173 
       
   174     def __init__(self, *args, **kwargs):
       
   175         super(AutomaticEntityForm, self).__init__(*args, **kwargs)
       
   176         entity = self.edited_entity
       
   177         if entity.has_eid():
       
   178             entity.complete()
       
   179         for rschema, role in self.editable_attributes():
       
   180             try:
       
   181                 self.field_by_name(rschema.type, role)
       
   182                 continue # explicitly specified
       
   183             except FieldNotFound:
       
   184                 # has to be guessed
       
   185                 try:
       
   186                     field = self.field_by_name(rschema.type, role,
       
   187                                                eschema=entity.e_schema)
       
   188                     self.fields.append(field)
       
   189                 except FieldNotFound:
       
   190                     # meta attribute such as <attr>_format
       
   191                     continue
       
   192         self.maxrelitems = self.req.property_value('navigation.related-limit')
       
   193         self.force_display = bool(self.req.form.get('__force_display'))
       
   194 
       
   195     @property
       
   196     def related_limit(self):
       
   197         if self.force_display:
       
   198             return None
       
   199         return self.maxrelitems + 1
       
   200 
       
   201     def relations_by_category(self, categories=None, permission=None):
       
   202         """return a list of (relation schema, target schemas, role) matching
       
   203         given category(ies) and permission
       
   204         """
       
   205         return self.erelations_by_category(self.edited_entity, categories,
       
   206                                            permission)
       
   207 
       
   208     def inlined_relations(self):
       
   209         """return a list of (relation schema, target schemas, role) matching
       
   210         given category(ies) and permission
       
   211         """
       
   212         # we'll need an initialized varmaker if there are some inlined relation
       
   213         self.initialize_varmaker()
       
   214         return self.erelations_by_category(self.edited_entity, True, 'add', self.rinlined)
       
   215 
       
   216     def srelations_by_category(self, categories=None, permission=None):
       
   217         """filter out result of relations_by_category(categories, permission) by
       
   218         removing final relations
       
   219 
       
   220         return a sorted list of (relation's label, relation'schema, role)
       
   221         """
       
   222         return self.esrelations_by_category(self.edited_entity, categories,
       
   223                                            permission)
       
   224 
       
   225     def action(self):
       
   226         """return the form's action attribute. Default to validateform if not
       
   227         explicitly overriden.
       
   228         """
       
   229         try:
       
   230             return self._action
       
   231         except AttributeError:
       
   232             return self.build_url('validateform')
       
   233 
       
   234     def set_action(self, value):
       
   235         """override default action"""
       
   236         self._action = value
       
   237 
       
   238     action = property(action, set_action)
       
   239 
       
   240     def editable_attributes(self):
       
   241         """return a list of (relation schema, role) to edit for the entity"""
       
   242         return [(rschema, x) for rschema, _, x in self.relations_by_category(
       
   243             self.attrcategories, 'add') if rschema != 'eid']
       
   244 
       
   245     def relations_table(self):
       
   246         """yiels 3-tuples (rtype, target, related_list)
       
   247         where <related_list> itself a list of :
       
   248           - node_id (will be the entity element's DOM id)
       
   249           - appropriate javascript's togglePendingDelete() function call
       
   250           - status 'pendingdelete' or ''
       
   251           - oneline view of related entity
       
   252         """
       
   253         entity = self.edited_entity
       
   254         pending_deletes = self.req.get_pending_deletes(entity.eid)
       
   255         for label, rschema, role in self.srelations_by_category('generic', 'add'):
       
   256             relatedrset = entity.related(rschema, role, limit=self.related_limit)
       
   257             if rschema.has_perm(self.req, 'delete'):
       
   258                 toggable_rel_link_func = toggable_relation_link
       
   259             else:
       
   260                 toggable_rel_link_func = lambda x, y, z: u''
       
   261             related = []
       
   262             for row in xrange(relatedrset.rowcount):
       
   263                 nodeid = relation_id(entity.eid, rschema, role,
       
   264                                      relatedrset[row][0])
       
   265                 if nodeid in pending_deletes:
       
   266                     status = u'pendingDelete'
       
   267                     label = '+'
       
   268                 else:
       
   269                     status = u''
       
   270                     label = 'x'
       
   271                 dellink = toggable_rel_link_func(entity.eid, nodeid, label)
       
   272                 eview = self.view('oneline', relatedrset, row=row)
       
   273                 related.append((nodeid, dellink, status, eview))
       
   274             yield (rschema, role, related)
       
   275 
       
   276     def restore_pending_inserts(self, cell=False):
       
   277         """used to restore edition page as it was before clicking on
       
   278         'search for <some entity type>'
       
   279         """
       
   280         eid = self.edited_entity.eid
       
   281         cell = cell and "div_insert_" or "tr"
       
   282         pending_inserts = set(self.req.get_pending_inserts(eid))
       
   283         for pendingid in pending_inserts:
       
   284             eidfrom, rtype, eidto = pendingid.split(':')
       
   285             if typed_eid(eidfrom) == eid: # subject
       
   286                 label = display_name(self.req, rtype, 'subject')
       
   287                 reid = eidto
       
   288             else:
       
   289                 label = display_name(self.req, rtype, 'object')
       
   290                 reid = eidfrom
       
   291             jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
       
   292                      % (pendingid, cell, eid)
       
   293             rset = self.req.eid_rset(reid)
       
   294             eview = self.view('text', rset, row=0)
       
   295             # XXX find a clean way to handle baskets
       
   296             if rset.description[0][0] == 'Basket':
       
   297                 eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
       
   298             yield rtype, pendingid, jscall, label, reid, eview
       
   299 
       
   300     # should_* method extracted to allow overriding
       
   301 
       
   302     def should_inline_relation_form(self, rschema, targettype, role):
       
   303         """return true if the given relation with entity has role and a
       
   304         targettype target should be inlined
       
   305         """
       
   306         return self.rinlined.etype_rtag(self.edited_entity.id, rschema, role, targettype)
       
   307 
       
   308     def should_display_inline_creation_form(self, rschema, existant, card):
       
   309         """return true if a creation form should be inlined
       
   310 
       
   311         by default true if there is no related entity and we need at least one
       
   312         """
       
   313         return not existant and card in '1+'
       
   314 
       
   315     def should_display_add_new_relation_link(self, rschema, existant, card):
       
   316         """return true if we should add a link to add a new creation form
       
   317         (through ajax call)
       
   318 
       
   319         by default true if there is no related entity or if the relation has
       
   320         multiple cardinality
       
   321         """
       
   322         return not existant or card in '+*'
       
   323 
       
   324 
       
   325 def etype_relation_field(etype, rtype, role='subject'):
       
   326     eschema = AutomaticEntityForm.schema.eschema(etype)
       
   327     return AutomaticEntityForm.field_by_name(rtype, role, eschema)