web/controller.py
author Sylvain Thenault <sylvain.thenault@logilab.fr>
Fri, 19 Dec 2008 14:53:40 +0100
changeset 232 f1f26070117d
parent 0 b97547f5f1fa
child 431 18b4dd650ef8
permissions -rw-r--r--
cleanup skel packaging

"""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)