diff -r 000000000000 -r b97547f5f1fa web/controller.py --- /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) +