web/form.py
changeset 0 b97547f5f1fa
child 431 18b4dd650ef8
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """abstract form classes for CubicWeb web client
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 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 simplejson import dumps
       
    10 
       
    11 from logilab.mtconverter import html_escape
       
    12 
       
    13 from cubicweb import typed_eid
       
    14 from cubicweb.common.selectors import req_form_params_selector
       
    15 from cubicweb.common.registerers import accepts_registerer
       
    16 from cubicweb.common.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
       
    17 from cubicweb.web import stdmsgs
       
    18 from cubicweb.web.httpcache import NoHTTPCacheManager
       
    19 from cubicweb.web.controller import redirect_params
       
    20 
       
    21 
       
    22 def relation_id(eid, rtype, target, reid):
       
    23     if target == 'subject':
       
    24         return u'%s:%s:%s' % (eid, rtype, reid)
       
    25     return u'%s:%s:%s' % (reid, rtype, eid)
       
    26 
       
    27 
       
    28 class FormMixIn(object):
       
    29     """abstract form mix-in"""
       
    30     category = 'form'
       
    31     controller = 'edit'
       
    32     domid = 'entityForm'
       
    33     
       
    34     http_cache_manager = NoHTTPCacheManager
       
    35     add_to_breadcrumbs = False
       
    36     skip_relations = set()
       
    37     
       
    38     def __init__(self, req, rset):
       
    39         super(FormMixIn, self).__init__(req, rset)
       
    40         self.maxrelitems = self.req.property_value('navigation.related-limit')
       
    41         self.maxcomboitems = self.req.property_value('navigation.combobox-limit')
       
    42         self.force_display = not not req.form.get('__force_display')
       
    43         # get validation session data which may have been previously set.
       
    44         # deleting validation errors here breaks form reloading (errors are
       
    45         # no more available), they have to be deleted by application's publish
       
    46         # method on successful commit
       
    47         formurl = req.url()
       
    48         forminfo = req.get_session_data(formurl)
       
    49         if forminfo:
       
    50             req.data['formvalues'] = forminfo['values']
       
    51             req.data['formerrors'] = errex = forminfo['errors']
       
    52             req.data['displayederrors'] = set()
       
    53             # if some validation error occured on entity creation, we have to
       
    54             # get the original variable name from its attributed eid
       
    55             foreid = errex.entity
       
    56             for var, eid in forminfo['eidmap'].items():
       
    57                 if foreid == eid:
       
    58                     errex.eid = var
       
    59                     break
       
    60             else:
       
    61                 errex.eid = foreid
       
    62         
       
    63     def html_headers(self):
       
    64         """return a list of html headers (eg something to be inserted between
       
    65         <head> and </head> of the returned page
       
    66 
       
    67         by default forms are neither indexed nor followed
       
    68         """
       
    69         return [NOINDEX, NOFOLLOW]
       
    70         
       
    71     def linkable(self):
       
    72         """override since forms are usually linked by an action,
       
    73         so we don't want them to be listed by appli.possible_views
       
    74         """
       
    75         return False
       
    76 
       
    77     @property
       
    78     def limit(self):
       
    79         if self.force_display:
       
    80             return None
       
    81         return self.maxrelitems + 1
       
    82 
       
    83     def need_multipart(self, entity, categories=('primary', 'secondary')):
       
    84         """return a boolean indicating if form's enctype should be multipart
       
    85         """
       
    86         for rschema, _, x in entity.relations_by_category(categories):
       
    87             if entity.get_widget(rschema, x).need_multipart:
       
    88                 return True
       
    89         # let's find if any of our inlined entities needs multipart
       
    90         for rschema, targettypes, x in entity.relations_by_category('inlineview'):
       
    91             assert len(targettypes) == 1, \
       
    92                    "I'm not able to deal with several targets and inlineview"
       
    93             ttype = targettypes[0]
       
    94             inlined_entity = self.vreg.etype_class(ttype)(self.req, None, None)
       
    95             for irschema, _, x in inlined_entity.relations_by_category(categories):
       
    96                 if inlined_entity.get_widget(irschema, x).need_multipart:
       
    97                     return True
       
    98         return False
       
    99 
       
   100     def error_message(self):
       
   101         """return formatted error message
       
   102 
       
   103         This method should be called once inlined field errors has been consumed
       
   104         """
       
   105         errex = self.req.data.get('formerrors')
       
   106         # get extra errors
       
   107         if errex is not None:
       
   108             errormsg = self.req._('please correct the following errors:')
       
   109             displayed = self.req.data['displayederrors']
       
   110             errors = sorted((field, err) for field, err in errex.errors.items()
       
   111                             if not field in displayed)
       
   112             if errors:
       
   113                 if len(errors) > 1:
       
   114                     templstr = '<li>%s</li>\n' 
       
   115                 else:
       
   116                     templstr = '&nbsp;%s\n'
       
   117                 for field, err in errors:
       
   118                     if field is None:
       
   119                         errormsg += templstr % err
       
   120                     else:
       
   121                         errormsg += templstr % '%s: %s' % (self.req._(field), err)
       
   122                 if len(errors) > 1:
       
   123                     errormsg = '<ul>%s</ul>' % errormsg
       
   124             return u'<div class="errorMessage">%s</div>' % errormsg
       
   125         return u''
       
   126     
       
   127     def restore_pending_inserts(self, entity, cell=False):
       
   128         """used to restore edition page as it was before clicking on
       
   129         'search for <some entity type>'
       
   130         
       
   131         """
       
   132         eid = entity.eid
       
   133         cell = cell and "div_insert_" or "tr"
       
   134         pending_inserts = set(self.req.get_pending_inserts(eid))
       
   135         for pendingid in pending_inserts:
       
   136             eidfrom, rtype, eidto = pendingid.split(':')
       
   137             if typed_eid(eidfrom) == entity.eid: # subject
       
   138                 label = display_name(self.req, rtype, 'subject')
       
   139                 reid = eidto
       
   140             else:
       
   141                 label = display_name(self.req, rtype, 'object')
       
   142                 reid = eidfrom
       
   143             jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
       
   144                      % (pendingid, cell, eid)
       
   145             rset = self.req.eid_rset(reid)
       
   146             eview = self.view('text', rset, row=0)
       
   147             # XXX find a clean way to handle baskets
       
   148             if rset.description[0][0] == 'Basket':
       
   149                 eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
       
   150             yield rtype, pendingid, jscall, label, reid, eview
       
   151         
       
   152     
       
   153     def force_display_link(self):
       
   154         return (u'<span class="invisible">' 
       
   155                 u'[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
       
   156                 u'</span>' % self.req._('view all'))
       
   157     
       
   158     def relations_table(self, entity):
       
   159         """yiels 3-tuples (rtype, target, related_list)
       
   160         where <related_list> itself a list of :
       
   161           - node_id (will be the entity element's DOM id)
       
   162           - appropriate javascript's togglePendingDelete() function call
       
   163           - status 'pendingdelete' or ''
       
   164           - oneline view of related entity
       
   165         """
       
   166         eid = entity.eid
       
   167         pending_deletes = self.req.get_pending_deletes(eid)
       
   168         # XXX (adim) : quick fix to get Folder relations
       
   169         for label, rschema, target in entity.srelations_by_category(('generic', 'metadata'), 'add'):
       
   170             if rschema in self.skip_relations:
       
   171                 continue
       
   172             relatedrset = entity.related(rschema, target, limit=self.limit)
       
   173             toggable_rel_link = self.toggable_relation_link_func(rschema)
       
   174             related = []
       
   175             for row in xrange(relatedrset.rowcount):
       
   176                 nodeid = relation_id(eid, rschema, target, relatedrset[row][0])
       
   177                 if nodeid in pending_deletes:
       
   178                     status = u'pendingDelete'
       
   179                     label = '+'
       
   180                 else:
       
   181                     status = u''
       
   182                     label = 'x'
       
   183                 dellink = toggable_rel_link(eid, nodeid, label)
       
   184                 eview = self.view('oneline', relatedrset, row=row)
       
   185                 related.append((nodeid, dellink, status, eview))
       
   186             yield (rschema, target, related)
       
   187         
       
   188     def toggable_relation_link_func(self, rschema):
       
   189         if not rschema.has_perm(self.req, 'delete'):
       
   190             return lambda x, y, z: u''
       
   191         return toggable_relation_link
       
   192 
       
   193 
       
   194     def redirect_url(self, entity=None):
       
   195         """return a url to use as next direction if there are some information
       
   196         specified in current form params, else return the result the reset_url
       
   197         method which should be defined in concrete classes
       
   198         """
       
   199         rparams = redirect_params(self.req.form)
       
   200         if rparams:
       
   201             return self.build_url('view', **rparams)
       
   202         return self.reset_url(entity)
       
   203 
       
   204     def reset_url(self, entity):
       
   205         raise NotImplementedError('implement me in concrete classes')
       
   206 
       
   207     BUTTON_STR = u'<input class="validateButton" type="submit" name="%s" value="%s" tabindex="%s"/>'
       
   208     ACTION_SUBMIT_STR = u'<input class="validateButton" type="button" onclick="postForm(\'%s\', \'%s\', \'%s\')" value="%s" tabindex="%s"/>'
       
   209 
       
   210     def button_ok(self, label=None, tabindex=None):
       
   211         label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
       
   212         return self.BUTTON_STR % ('defaultsubmit', label, tabindex or 2)
       
   213     
       
   214     def button_apply(self, label=None, tabindex=None):
       
   215         label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
       
   216         return self.ACTION_SUBMIT_STR % ('__action_apply', label, self.domid, label, tabindex or 3)
       
   217 
       
   218     def button_delete(self, label=None, tabindex=None):
       
   219         label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
       
   220         return self.ACTION_SUBMIT_STR % ('__action_delete', label, self.domid, label, tabindex or 3)
       
   221     
       
   222     def button_cancel(self, label=None, tabindex=None):
       
   223         label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
       
   224         return self.ACTION_SUBMIT_STR % ('__action_cancel', label, self.domid, label, tabindex or 4)
       
   225     
       
   226     def button_reset(self, label=None, tabindex=None):
       
   227         label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
       
   228         return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
       
   229             label, tabindex or 4)
       
   230         
       
   231 def toggable_relation_link(eid, nodeid, label='x'):
       
   232     js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
       
   233     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
       
   234 
       
   235 
       
   236 class Form(FormMixIn, View):
       
   237     """base class for forms. Apply by default according to request form
       
   238     parameters specified using the `form_params` class attribute which
       
   239     should list necessary parameters in the form to be accepted.
       
   240     """
       
   241     __registerer__ = accepts_registerer
       
   242     __select__ = classmethod(req_form_params_selector)
       
   243 
       
   244     form_params = ()
       
   245 
       
   246 class EntityForm(FormMixIn, EntityView):
       
   247     """base class for forms applying on an entity (i.e. uniform result set)
       
   248     """
       
   249 
       
   250 class AnyRsetForm(FormMixIn, AnyRsetView):
       
   251     """base class for forms applying on any empty result sets
       
   252     """
       
   253