web/controller.py
changeset 0 b97547f5f1fa
child 431 18b4dd650ef8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/controller.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,257 @@
+"""abstract controler classe for CubicWeb web client
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from mx.DateTime import strptime, Error as MxDTError, TimeDelta
+
+from cubicweb import typed_eid
+from cubicweb.common.registerers import priority_registerer
+from cubicweb.common.selectors import in_group_selector
+from cubicweb.common.appobject import AppObject
+from cubicweb.web import LOGGER, Redirect, RequestError
+
+
+NAVIGATION_PARAMETERS = (('vid', '__redirectvid'),
+                         ('rql', '__redirectrql'),
+                         ('__redirectpath', '__redirectpath'),
+                         ('__redirectparams', '__redirectparams'),
+                         )
+NAV_FORM_PARAMETERS = [fp for ap, fp in NAVIGATION_PARAMETERS]
+
+def redirect_params(form):
+    """transform redirection parameters into navigation parameters
+    """
+    params = {}
+    # extract navigation parameters from redirection parameters
+    for navparam, redirectparam in NAVIGATION_PARAMETERS:
+        if navparam == redirectparam:
+            continue
+        if redirectparam in form:
+            params[navparam] = form[redirectparam]
+    return params
+
+def parse_relations_descr(rdescr):
+    """parse a string describing some relations, in the form
+    subjeids:rtype:objeids
+    where subjeids and objeids are eids separeted by a underscore
+
+    return an iterator on (subject eid, relation type, object eid) found
+    """
+    for rstr in rdescr:
+        subjs, rtype, objs = rstr.split(':')
+        for subj in subjs.split('_'):
+            for obj in objs.split('_'):
+                yield typed_eid(subj), rtype, typed_eid(obj)
+        
+def append_url_params(url, params):
+    """append raw parameters to the url. Given parameters, if any, are expected
+    to be already url-quoted.
+    """
+    if params:
+        if not '?' in url:
+            url += '?'
+        else:
+            url += '&'
+        url += params
+    return url
+
+
+class Controller(AppObject):
+    """a controller is responsible to make necessary stuff to publish
+    a request. There is usually at least one standard "view" controller
+    and another linked by forms to edit objects ("edit").
+    """
+    __registry__ = 'controllers'
+    __registerer__ = priority_registerer
+    __selectors__ = (in_group_selector,)
+    require_groups = ()
+
+    def __init__(self, *args, **kwargs):
+        super(Controller, self).__init__(*args, **kwargs)
+        # attributes use to control after edition redirection
+        self._after_deletion_path = None
+        self._edited_entity = None
+        
+    def publish(self, rset=None):
+        """publish the current request, with an option input rql string
+        (already processed if necessary)
+        """
+        raise NotImplementedError
+
+    # generic methods useful for concret implementations ######################
+    
+    def check_expected_params(self, params):
+        """check that the given list of parameters are specified in the form
+        dictionary
+        """
+        missing = []
+        for param in params:
+            if not self.req.form.get(param):
+                missing.append(param)
+        if missing:
+            raise RequestError('missing required parameter(s): %s'
+                               % ','.join(missing))
+    
+    def parse_datetime(self, value, etype='Datetime'):
+        """get a datetime or time from a string (according to etype)
+        Datetime formatted as Date are accepted
+        """
+        assert etype in ('Datetime', 'Date', 'Time'), etype
+        # XXX raise proper validation error
+        if etype == 'Datetime':
+            format = self.req.property_value('ui.datetime-format')
+            try:
+                return strptime(value, format)
+            except MxDTError:
+                pass
+        elif etype == 'Time':
+            format = self.req.property_value('ui.time-format')
+            try:
+                # (adim) I can't find a way to parse a Time with a custom format
+                date = strptime(value, format) # this returns a DateTime
+                return TimeDelta(date.hour, date.minute, date.second)
+            except MxDTError:
+                raise ValueError('can\'t parse %r (expected %s)' % (value, format))
+        try:
+            format = self.req.property_value('ui.date-format')
+            return strptime(value, format)
+        except MxDTError:
+            raise ValueError('can\'t parse %r (expected %s)' % (value, format))
+
+
+    def notify_edited(self, entity):
+        """called by edit_entity() to notify which entity is edited"""
+        # NOTE: we can't use entity.rest_path() at this point because
+        #       rest_path() could rely on schema constraints (such as a required
+        #       relation) that might not be satisfied yet (in case of creations)
+        if not self._edited_entity:
+            self._edited_entity = entity
+        
+    def delete_entities(self, eidtypes):
+        """delete entities from the repository"""
+        redirect_info = set()
+        eidtypes = tuple(eidtypes)
+        for eid, etype in eidtypes:
+            entity = self.req.eid_rset(eid, etype).get_entity(0, 0)
+            path, params = entity.after_deletion_path()
+            redirect_info.add( (path, tuple(params.iteritems())) )
+            entity.delete()
+        if len(redirect_info) > 1:
+            # In the face of ambiguity, refuse the temptation to guess.
+            self._after_deletion_path = 'view', ()
+        else:
+            self._after_deletion_path = iter(redirect_info).next()
+        if len(eidtypes) > 1:
+            self.req.set_message(self.req._('entities deleted'))
+        else:
+            self.req.set_message(self.req._('entity deleted'))
+        
+    def delete_relations(self, rdefs):
+        """delete relations from the repository"""
+        # FIXME convert to using the syntax subject:relation:eids
+        execute = self.req.execute
+        for subj, rtype, obj in rdefs:
+            rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
+            execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
+        self.req.set_message(self.req._('relations deleted'))
+    
+    def insert_relations(self, rdefs):
+        """insert relations into the repository"""
+        execute = self.req.execute
+        for subj, rtype, obj in rdefs:
+            rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype
+            execute(rql, {'x': subj, 'y': obj}, ('x', 'y'))
+
+    
+    def reset(self):
+        """reset form parameters and redirect to a view determinated by given
+        parameters
+        """
+        newparams = {}
+        # sets message if needed
+        if self.req.message:
+            newparams['__message'] = self.req.message
+        if self.req.form.has_key('__action_apply'):
+            self._return_to_edition_view(newparams)
+        if self.req.form.has_key('__action_cancel'):
+            self._return_to_lastpage(newparams)
+        else:
+            self._return_to_original_view(newparams)
+
+
+    def _return_to_original_view(self, newparams):
+        """validate-button case"""
+        # transforms __redirect[*] parameters into regular form parameters
+        newparams.update(redirect_params(self.req.form))
+        # find out if we have some explicit `rql` needs
+        rql = newparams.pop('rql', None)
+        # if rql is needed (explicit __redirectrql or multiple deletions for
+        # instance), we have to use the old `view?rql=...` form
+        if rql:
+            path = 'view'
+            newparams['rql'] = rql
+        elif '__redirectpath' in self.req.form:
+            # if redirect path was explicitly specified in the form, use it
+            path = self.req.form['__redirectpath']
+        elif self._after_deletion_path:
+            # else it should have been set during form processing
+            path, params = self._after_deletion_path
+            params = dict(params) # params given as tuple
+            params.update(newparams)
+            newparams = params
+        elif self._edited_entity:
+            path = self._edited_entity.rest_path()
+        else:
+            path = 'view'
+        url = self.build_url(path, **newparams)
+        url = append_url_params(url, self.req.form.get('__redirectparams'))
+        raise Redirect(url)
+    
+
+    def _return_to_edition_view(self, newparams):
+        """apply-button case"""
+        form = self.req.form
+        if self._edited_entity:
+            path = self._edited_entity.rest_path()
+            newparams.pop('rql', None)
+        # else, fallback on the old `view?rql=...` url form
+        elif 'rql' in self.req.form:
+            path = 'view'
+            newparams['rql'] = form['rql']
+        else:
+            self.warning("the edited data seems inconsistent")
+            path = 'view'
+        # pick up the correction edition view
+        if form.get('__form_id'):
+            newparams['vid'] = form['__form_id']
+        # re-insert copy redirection parameters
+        for redirectparam in NAV_FORM_PARAMETERS:
+            if redirectparam in form:
+                newparams[redirectparam] = form[redirectparam]
+        raise Redirect(self.build_url(path, **newparams))
+
+
+    def _return_to_lastpage(self, newparams):
+        """cancel-button case: in this case we are always expecting to go back
+        where we came from, and this is not easy. Currently we suppose that
+        __redirectpath is specifying that place if found, else we look in the
+        request breadcrumbs for the last visited page.
+        """
+        if '__redirectpath' in self.req.form:
+            # if redirect path was explicitly specified in the form, use it
+            path = self.req.form['__redirectpath']
+            url = self.build_url(path, **newparams)
+            url = append_url_params(url, self.req.form.get('__redirectparams'))
+        else:
+            url = self.req.last_visited_page()
+        raise Redirect(url)
+
+
+from cubicweb import set_log_methods
+set_log_methods(Controller, LOGGER)
+