web/controller.py
changeset 0 b97547f5f1fa
child 431 18b4dd650ef8
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """abstract controler classe for CubicWeb web client
       
     2 
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from mx.DateTime import strptime, Error as MxDTError, TimeDelta
       
    11 
       
    12 from cubicweb import typed_eid
       
    13 from cubicweb.common.registerers import priority_registerer
       
    14 from cubicweb.common.selectors import in_group_selector
       
    15 from cubicweb.common.appobject import AppObject
       
    16 from cubicweb.web import LOGGER, Redirect, RequestError
       
    17 
       
    18 
       
    19 NAVIGATION_PARAMETERS = (('vid', '__redirectvid'),
       
    20                          ('rql', '__redirectrql'),
       
    21                          ('__redirectpath', '__redirectpath'),
       
    22                          ('__redirectparams', '__redirectparams'),
       
    23                          )
       
    24 NAV_FORM_PARAMETERS = [fp for ap, fp in NAVIGATION_PARAMETERS]
       
    25 
       
    26 def redirect_params(form):
       
    27     """transform redirection parameters into navigation parameters
       
    28     """
       
    29     params = {}
       
    30     # extract navigation parameters from redirection parameters
       
    31     for navparam, redirectparam in NAVIGATION_PARAMETERS:
       
    32         if navparam == redirectparam:
       
    33             continue
       
    34         if redirectparam in form:
       
    35             params[navparam] = form[redirectparam]
       
    36     return params
       
    37 
       
    38 def parse_relations_descr(rdescr):
       
    39     """parse a string describing some relations, in the form
       
    40     subjeids:rtype:objeids
       
    41     where subjeids and objeids are eids separeted by a underscore
       
    42 
       
    43     return an iterator on (subject eid, relation type, object eid) found
       
    44     """
       
    45     for rstr in rdescr:
       
    46         subjs, rtype, objs = rstr.split(':')
       
    47         for subj in subjs.split('_'):
       
    48             for obj in objs.split('_'):
       
    49                 yield typed_eid(subj), rtype, typed_eid(obj)
       
    50         
       
    51 def append_url_params(url, params):
       
    52     """append raw parameters to the url. Given parameters, if any, are expected
       
    53     to be already url-quoted.
       
    54     """
       
    55     if params:
       
    56         if not '?' in url:
       
    57             url += '?'
       
    58         else:
       
    59             url += '&'
       
    60         url += params
       
    61     return url
       
    62 
       
    63 
       
    64 class Controller(AppObject):
       
    65     """a controller is responsible to make necessary stuff to publish
       
    66     a request. There is usually at least one standard "view" controller
       
    67     and another linked by forms to edit objects ("edit").
       
    68     """
       
    69     __registry__ = 'controllers'
       
    70     __registerer__ = priority_registerer
       
    71     __selectors__ = (in_group_selector,)
       
    72     require_groups = ()
       
    73 
       
    74     def __init__(self, *args, **kwargs):
       
    75         super(Controller, self).__init__(*args, **kwargs)
       
    76         # attributes use to control after edition redirection
       
    77         self._after_deletion_path = None
       
    78         self._edited_entity = None
       
    79         
       
    80     def publish(self, rset=None):
       
    81         """publish the current request, with an option input rql string
       
    82         (already processed if necessary)
       
    83         """
       
    84         raise NotImplementedError
       
    85 
       
    86     # generic methods useful for concret implementations ######################
       
    87     
       
    88     def check_expected_params(self, params):
       
    89         """check that the given list of parameters are specified in the form
       
    90         dictionary
       
    91         """
       
    92         missing = []
       
    93         for param in params:
       
    94             if not self.req.form.get(param):
       
    95                 missing.append(param)
       
    96         if missing:
       
    97             raise RequestError('missing required parameter(s): %s'
       
    98                                % ','.join(missing))
       
    99     
       
   100     def parse_datetime(self, value, etype='Datetime'):
       
   101         """get a datetime or time from a string (according to etype)
       
   102         Datetime formatted as Date are accepted
       
   103         """
       
   104         assert etype in ('Datetime', 'Date', 'Time'), etype
       
   105         # XXX raise proper validation error
       
   106         if etype == 'Datetime':
       
   107             format = self.req.property_value('ui.datetime-format')
       
   108             try:
       
   109                 return strptime(value, format)
       
   110             except MxDTError:
       
   111                 pass
       
   112         elif etype == 'Time':
       
   113             format = self.req.property_value('ui.time-format')
       
   114             try:
       
   115                 # (adim) I can't find a way to parse a Time with a custom format
       
   116                 date = strptime(value, format) # this returns a DateTime
       
   117                 return TimeDelta(date.hour, date.minute, date.second)
       
   118             except MxDTError:
       
   119                 raise ValueError('can\'t parse %r (expected %s)' % (value, format))
       
   120         try:
       
   121             format = self.req.property_value('ui.date-format')
       
   122             return strptime(value, format)
       
   123         except MxDTError:
       
   124             raise ValueError('can\'t parse %r (expected %s)' % (value, format))
       
   125 
       
   126 
       
   127     def notify_edited(self, entity):
       
   128         """called by edit_entity() to notify which entity is edited"""
       
   129         # NOTE: we can't use entity.rest_path() at this point because
       
   130         #       rest_path() could rely on schema constraints (such as a required
       
   131         #       relation) that might not be satisfied yet (in case of creations)
       
   132         if not self._edited_entity:
       
   133             self._edited_entity = entity
       
   134         
       
   135     def delete_entities(self, eidtypes):
       
   136         """delete entities from the repository"""
       
   137         redirect_info = set()
       
   138         eidtypes = tuple(eidtypes)
       
   139         for eid, etype in eidtypes:
       
   140             entity = self.req.eid_rset(eid, etype).get_entity(0, 0)
       
   141             path, params = entity.after_deletion_path()
       
   142             redirect_info.add( (path, tuple(params.iteritems())) )
       
   143             entity.delete()
       
   144         if len(redirect_info) > 1:
       
   145             # In the face of ambiguity, refuse the temptation to guess.
       
   146             self._after_deletion_path = 'view', ()
       
   147         else:
       
   148             self._after_deletion_path = iter(redirect_info).next()
       
   149         if len(eidtypes) > 1:
       
   150             self.req.set_message(self.req._('entities deleted'))
       
   151         else:
       
   152             self.req.set_message(self.req._('entity deleted'))
       
   153         
       
   154     def delete_relations(self, rdefs):
       
   155         """delete relations from the repository"""
       
   156         # FIXME convert to using the syntax subject:relation:eids
       
   157         execute = self.req.execute
       
   158         for subj, rtype, obj in rdefs:
       
   159             rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
       
   160             execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
       
   161         self.req.set_message(self.req._('relations deleted'))
       
   162     
       
   163     def insert_relations(self, rdefs):
       
   164         """insert relations into the repository"""
       
   165         execute = self.req.execute
       
   166         for subj, rtype, obj in rdefs:
       
   167             rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
       
   168             execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
       
   169 
       
   170     
       
   171     def reset(self):
       
   172         """reset form parameters and redirect to a view determinated by given
       
   173         parameters
       
   174         """
       
   175         newparams = {}
       
   176         # sets message if needed
       
   177         if self.req.message:
       
   178             newparams['__message'] = self.req.message
       
   179         if self.req.form.has_key('__action_apply'):
       
   180             self._return_to_edition_view(newparams)
       
   181         if self.req.form.has_key('__action_cancel'):
       
   182             self._return_to_lastpage(newparams)
       
   183         else:
       
   184             self._return_to_original_view(newparams)
       
   185 
       
   186 
       
   187     def _return_to_original_view(self, newparams):
       
   188         """validate-button case"""
       
   189         # transforms __redirect[*] parameters into regular form parameters
       
   190         newparams.update(redirect_params(self.req.form))
       
   191         # find out if we have some explicit `rql` needs
       
   192         rql = newparams.pop('rql', None)
       
   193         # if rql is needed (explicit __redirectrql or multiple deletions for
       
   194         # instance), we have to use the old `view?rql=...` form
       
   195         if rql:
       
   196             path = 'view'
       
   197             newparams['rql'] = rql
       
   198         elif '__redirectpath' in self.req.form:
       
   199             # if redirect path was explicitly specified in the form, use it
       
   200             path = self.req.form['__redirectpath']
       
   201         elif self._after_deletion_path:
       
   202             # else it should have been set during form processing
       
   203             path, params = self._after_deletion_path
       
   204             params = dict(params) # params given as tuple
       
   205             params.update(newparams)
       
   206             newparams = params
       
   207         elif self._edited_entity:
       
   208             path = self._edited_entity.rest_path()
       
   209         else:
       
   210             path = 'view'
       
   211         url = self.build_url(path, **newparams)
       
   212         url = append_url_params(url, self.req.form.get('__redirectparams'))
       
   213         raise Redirect(url)
       
   214     
       
   215 
       
   216     def _return_to_edition_view(self, newparams):
       
   217         """apply-button case"""
       
   218         form = self.req.form
       
   219         if self._edited_entity:
       
   220             path = self._edited_entity.rest_path()
       
   221             newparams.pop('rql', None)
       
   222         # else, fallback on the old `view?rql=...` url form
       
   223         elif 'rql' in self.req.form:
       
   224             path = 'view'
       
   225             newparams['rql'] = form['rql']
       
   226         else:
       
   227             self.warning("the edited data seems inconsistent")
       
   228             path = 'view'
       
   229         # pick up the correction edition view
       
   230         if form.get('__form_id'):
       
   231             newparams['vid'] = form['__form_id']
       
   232         # re-insert copy redirection parameters
       
   233         for redirectparam in NAV_FORM_PARAMETERS:
       
   234             if redirectparam in form:
       
   235                 newparams[redirectparam] = form[redirectparam]
       
   236         raise Redirect(self.build_url(path, **newparams))
       
   237 
       
   238 
       
   239     def _return_to_lastpage(self, newparams):
       
   240         """cancel-button case: in this case we are always expecting to go back
       
   241         where we came from, and this is not easy. Currently we suppose that
       
   242         __redirectpath is specifying that place if found, else we look in the
       
   243         request breadcrumbs for the last visited page.
       
   244         """
       
   245         if '__redirectpath' in self.req.form:
       
   246             # if redirect path was explicitly specified in the form, use it
       
   247             path = self.req.form['__redirectpath']
       
   248             url = self.build_url(path, **newparams)
       
   249             url = append_url_params(url, self.req.form.get('__redirectparams'))
       
   250         else:
       
   251             url = self.req.last_visited_page()
       
   252         raise Redirect(url)
       
   253 
       
   254 
       
   255 from cubicweb import set_log_methods
       
   256 set_log_methods(Controller, LOGGER)
       
   257