diff -r 000000000000 -r b97547f5f1fa web/views/basecontrollers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/views/basecontrollers.py Wed Nov 05 15:52:50 2008 +0100 @@ -0,0 +1,515 @@ +# -*- coding: utf-8 -*- +"""Set of base controllers, which are directly plugged into the application +object to handle publication. + + +: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 smtplib import SMTP + +import simplejson + +from mx.DateTime.Parser import DateFromString + +from logilab.common.decorators import cached + +from cubicweb import NoSelectableObject, ValidationError, typed_eid +from cubicweb.common.selectors import yes_selector +from cubicweb.common.mail import format_mail +from cubicweb.common.view import STRICT_DOCTYPE, CW_XHTML_EXTENSIONS + +from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed +from cubicweb.web.controller import Controller +from cubicweb.web.views import vid_from_rset +try: + from cubicweb.web.facet import (FilterRQLBuilder, get_facet, + prepare_facets_rqlst) + HAS_SEARCH_RESTRICTION = True +except ImportError: # gae + HAS_SEARCH_RESTRICTION = False + + +class LoginController(Controller): + id = 'login' + + def publish(self, rset=None): + """log in the application""" + if self.config['auth-mode'] == 'http': + # HTTP authentication + raise ExplicitLogin() + else: + # Cookie authentication + return self.appli.need_login_content(self.req) + + +class LogoutController(Controller): + id = 'logout' + + def publish(self, rset=None): + """logout from the application""" + return self.appli.session_handler.logout(self.req) + + +class ViewController(Controller): + id = 'view' + template = 'main' + + def publish(self, rset=None): + """publish a request, returning an encoded string""" + self.req.update_search_state() + template = self.req.property_value('ui.main-template') + if template not in self.vreg.registry('templates') : + template = self.template + return self.vreg.main_template(self.req, template, rset=rset) + + def execute_linkto(self, eid=None): + """XXX __linkto parameter may cause security issue + + defined here since custom application controller inheriting from this + one use this method? + """ + req = self.req + if not '__linkto' in req.form: + return + if eid is None: + eid = typed_eid(req.form['eid']) + for linkto in req.list_form_param('__linkto', pop=True): + rtype, eids, target = linkto.split(':') + assert target in ('subject', 'object') + eids = eids.split('_') + if target == 'subject': + rql = 'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype + else: + rql = 'SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s' % rtype + for teid in eids: + req.execute(rql, {'x': eid, 'y': typed_eid(teid)}, ('x', 'y')) + + +class FormValidatorController(Controller): + id = 'validateform' + + def publish(self, rset=None): + vreg = self.vreg + try: + ctrl = vreg.select(vreg.registry_objects('controllers', 'edit'), + req=self.req, appli=self.appli) + except NoSelectableObject: + status, args = (False, {None: self.req._('not authorized')}) + else: + try: + ctrl.publish(None, fromjson=True) + except ValidationError, err: + status, args = self.validation_error(err) + except Redirect, err: + try: + self.req.cnx.commit() # ValidationError may be raise on commit + except ValidationError, err: + status, args = self.validation_error(err) + else: + status, args = (True, err.location) + except Exception, err: + self.req.cnx.rollback() + self.exception('unexpected error in validateform') + try: + status, args = (False, self.req._(unicode(err))) + except UnicodeError: + status, args = (False, repr(err)) + else: + status, args = (False, '???') + self.req.set_content_type('text/html') + jsarg = simplejson.dumps( (status, args) ) + return """""" % simplejson.dumps( (status, args) ) + + def validation_error(self, err): + self.req.cnx.rollback() + try: + eid = err.entity.eid + except AttributeError: + eid = err.entity + return (False, (eid, err.errors)) + +def xmlize(source): + head = u'\n' + STRICT_DOCTYPE % CW_XHTML_EXTENSIONS + return head + u'
%s
' % source.strip() + +def jsonize(func): + """sets correct content_type and calls `simplejson.dumps` on results + """ + def wrapper(self, *args, **kwargs): + self.req.set_content_type('application/json') + result = func(self, *args, **kwargs) + return simplejson.dumps(result) + return wrapper + + +def check_pageid(func): + """decorator which checks the given pageid is found in the + user's session data + """ + def wrapper(self, *args, **kwargs): + data = self.req.get_session_data(self.req.pageid) + if data is None: + raise RemoteCallFailed(self.req._('pageid-not-found')) + return func(self, *args, **kwargs) + return wrapper + + +class JSonController(Controller): + id = 'json' + template = 'main' + + def publish(self, rset=None): + mode = self.req.form.get('mode', 'html') + self.req.pageid = self.req.form.get('pageid') + try: + func = getattr(self, '%s_exec' % mode) + except AttributeError, ex: + self.error('json controller got an unknown mode %r', mode) + self.error('\t%s', ex) + result = u'' + else: + try: + result = func(rset) + except RemoteCallFailed: + raise + except Exception, ex: + self.exception('an exception occured on json request(rset=%s): %s', + rset, ex) + raise RemoteCallFailed(repr(ex)) + return result.encode(self.req.encoding) + + def _exec(self, rql, args=None, eidkey=None, rocheck=True): + """json mode: execute RQL and return resultset as json""" + if rocheck: + self.ensure_ro_rql(rql) + try: + return self.req.execute(rql, args, eidkey) + except Exception, ex: + self.exception("error in _exec(rql=%s): %s", rql, ex) + return None + return None + + @jsonize + def json_exec(self, rset=None): + """json mode: execute RQL and return resultset as json""" + rql = self.req.form.get('rql') + if rset is None and rql: + rset = self._exec(rql) + return rset and rset.rows or [] + + def _set_content_type(self, vobj, data): + """sets req's content type according to vobj's content type + (and xmlize data if needed) + """ + content_type = vobj.content_type + if content_type == 'application/xhtml+xml': + self.req.set_content_type(content_type) + return xmlize(data) + return data + + def html_exec(self, rset=None): + """html mode: execute query and return the view as HTML""" + req = self.req + rql = req.form.get('rql') + if rset is None and rql: + rset = self._exec(rql) + + vid = req.form.get('vid') or vid_from_rset(req, rset, self.schema) + try: + view = self.vreg.select_view(vid, req, rset) + except NoSelectableObject: + vid = req.form.get('fallbackvid', 'noresult') + view = self.vreg.select_view(vid, req, rset) + divid = req.form.get('divid', 'pageContent') + # we need to call pagination before with the stream set + stream = view.set_stream() + if req.form.get('paginate'): + if divid == 'pageContent': + # mimick main template behaviour + stream.write(u'
') + vtitle = self.req.form.get('vtitle') + if vtitle: + w(u'

%s

\n' % vtitle) + view.pagination(req, rset, view.w, not view.need_navigation) + if divid == 'pageContent': + stream.write(u'
') + view.dispatch() + if req.form.get('paginate') and divid == 'pageContent': + stream.write(u'
') + source = stream.getvalue() + return self._set_content_type(view, source) + + def rawremote_exec(self, rset=None): + """like remote_exec but doesn't change content type""" + # no attribute means the callback takes no argument + args = self.req.form.get('arg', ()) + if not isinstance(args, (list, tuple)): + args = (args,) + fname = self.req.form['fname'] + args = [simplejson.loads(arg) for arg in args] + try: + func = getattr(self, 'js_%s' % fname) + except AttributeError: + self.exception('rawremote_exec fname=%s', fname) + return u"" + return func(*args) + + remote_exec = jsonize(rawremote_exec) + + def _rebuild_posted_form(self, names, values, action=None): + form = {} + for name, value in zip(names, values): + # remove possible __action_xxx inputs + if name.startswith('__action'): + continue + # form.setdefault(name, []).append(value) + if name in form: + curvalue = form[name] + if isinstance(curvalue, list): + curvalue.append(value) + else: + form[name] = [curvalue, value] + else: + form[name] = value + # simulate click on __action_%s button to help the controller + if action: + form['__action_%s' % action] = u'whatever' + return form + + def js_validate_form(self, action, names, values): + # XXX this method (and correspoding js calls) should use the new + # `RemoteCallFailed` mechansim + self.req.form = self._rebuild_posted_form(names, values, action) + vreg = self.vreg + try: + ctrl = vreg.select(vreg.registry_objects('controllers', 'edit'), + req=self.req) + except NoSelectableObject: + return (False, {None: self.req._('not authorized')}) + try: + ctrl.publish(None, fromjson=True) + except ValidationError, err: + self.req.cnx.rollback() + if not err.entity or isinstance(err.entity, (long, int)): + eid = err.entity + else: + eid = err.entity.eid + return (False, (eid, err.errors)) + except Redirect, err: + return (True, err.location) + except Exception, err: + self.req.cnx.rollback() + self.exception('unexpected error in js_validateform') + return (False, self.req._(str(err))) + return (False, '???') + + def js_edit_field(self, action, names, values, rtype, eid): + success, args = self.js_validate_form(action, names, values) + if success: + rset = self.req.execute('Any X,N WHERE X eid %%(x)s, X %s N' % rtype, + {'x': eid}, 'x') + entity = rset.get_entity(0, 0) + return (success, args, entity.printable_value(rtype)) + else: + return (success, args, None) + + def js_rql(self, rql): + rset = self._exec(rql) + return rset and rset.rows or [] + + def js_i18n(self, msgids): + """returns the translation of `msgid`""" + return [self.req._(msgid) for msgid in msgids] + + def js_format_date(self, strdate): + """returns the formatted date for `msgid`""" + date = DateFromString(strdate) + return self.format_date(date) + + def js_external_resource(self, resource): + """returns the URL of the external resource named `resource`""" + return self.req.external_resource(resource) + + def js_prop_widget(self, propkey, varname, tabindex=None): + """specific method for EProperty handling""" + w = self.vreg.property_value_widget(propkey, req=self.req) + entity = self.vreg.etype_class('EProperty')(self.req, None, None) + entity.eid = varname + self.req.form['value'] = self.vreg.property_info(propkey)['default'] + return w.edit_render(entity, tabindex, includehelp=True) + + def js_component(self, compid, rql, registry='components', extraargs=None): + if rql: + rset = self._exec(rql) + else: + rset = None + comp = self.vreg.select_object(registry, compid, self.req, rset) + if extraargs is None: + extraargs = {} + else: # we receive unicode keys which is not supported by the **syntax + extraargs = dict((str(key), value) + for key, value in extraargs.items()) + extraargs = extraargs or {} + print 'extraargs =', extraargs + return self._set_content_type(comp, comp.dispatch(**extraargs)) + + @check_pageid + def js_user_callback(self, cbname): + page_data = self.req.get_session_data(self.req.pageid, {}) + try: + cb = page_data[cbname] + except KeyError: + return None + return cb(self.req) + + def js_unregister_user_callback(self, cbname): + self.req.unregister_callback(self.req.pageid, cbname) + + def js_unload_page_data(self): + self.req.del_session_data(self.req.pageid) + + def js_cancel_edition(self, errorurl): + """cancelling edition from javascript + + We need to clear associated req's data : + - errorurl + - pending insertions / deletions + """ + self.req.cancel_edition(errorurl) + + @check_pageid + def js_inline_creation_form(self, peid, ptype, ttype, rtype, role): + view = self.vreg.select_view('inline-creation', self.req, None, + etype=ttype, ptype=ptype, peid=peid, + rtype=rtype, role=role) + source = view.dispatch(etype=ttype, ptype=ptype, peid=peid, rtype=rtype, + role=role) + return self._set_content_type(view, source) + + def js_remove_pending_insert(self, (eidfrom, rel, eidto)): + self._remove_pending(eidfrom, rel, eidto, 'insert') + + def js_add_pending_insert(self, (eidfrom, rel, eidto)): + self._add_pending(eidfrom, rel, eidto, 'insert') + + def js_add_pending_inserts(self, tripletlist): + for eidfrom, rel, eidto in tripletlist: + self._add_pending(eidfrom, rel, eidto, 'insert') + + def js_remove_pending_delete(self, (eidfrom, rel, eidto)): + self._remove_pending(eidfrom, rel, eidto, 'delete') + + def js_add_pending_delete(self, (eidfrom, rel, eidto)): + self._add_pending(eidfrom, rel, eidto, 'delete') + + if HAS_SEARCH_RESTRICTION: + def js_filter_build_rql(self, names, values): + form = self._rebuild_posted_form(names, values) + self.req.form = form + builder = FilterRQLBuilder(self.req) + return builder.build_rql() + + def js_filter_select_content(self, facetids, rql): + rqlst = self.vreg.parse(self.req, rql) # XXX Union unsupported yet + mainvar = prepare_facets_rqlst(rqlst)[0] + update_map = {} + for facetid in facetids: + facet = get_facet(self.req, facetid, rqlst.children[0], mainvar) + update_map[facetid] = facet.possible_values() + return update_map + + def js_delete_bookmark(self, beid): + try: + rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s' + self.req.execute(rql, {'b': typed_eid(beid), 'u' : self.req.user.eid}) + except Exception, ex: + self.exception(unicode(ex)) + return self.req._('Problem occured') + + def _add_pending(self, eidfrom, rel, eidto, kind): + key = 'pending_%s' % kind + pendings = self.req.get_session_data(key, set()) + pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) + self.req.set_session_data(key, pendings) + + def _remove_pending(self, eidfrom, rel, eidto, kind): + key = 'pending_%s' % kind + try: + pendings = self.req.get_session_data(key) + pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) + except: + self.exception('while removing pending eids') + else: + self.req.set_session_data(key, pendings) + + def js_add_and_link_new_entity(self, etype_to, rel, eid_to, etype_from, value_from): + # create a new entity + eid_from = self.req.execute('INSERT %s T : T name "%s"' % ( etype_from, value_from ))[0][0] + # link the new entity to the main entity + rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from} + return eid_from + +class SendMailController(Controller): + id = 'sendmail' + require_groups = ('managers', 'users') + + def recipients(self): + """returns an iterator on email's recipients as entities""" + eids = self.req.form['recipient'] + # make sure we have a list even though only one recipient was specified + if isinstance(eids, basestring): + eids = (eids,) + rql = 'Any X WHERE X eid in (%s)' % (','.join(eids)) + rset = self.req.execute(rql) + for entity in rset.entities(): + entity.complete() # XXX really? + yield entity + + @property + @cached + def smtp(self): + mailhost, port = self.config['smtp-host'], self.config['smtp-port'] + try: + return SMTP(mailhost, port) + except Exception, ex: + self.exception("can't connect to smtp server %s:%s (%s)", + mailhost, port, ex) + url = self.build_url(__message=self.req._('could not connect to the SMTP server')) + raise Redirect(url) + + def sendmail(self, recipient, subject, body): + helo_addr = '%s <%s>' % (self.config['sender-name'], + self.config['sender-addr']) + msg = format_mail({'email' : self.req.user.get_email(), + 'name' : self.req.user.dc_title(),}, + [recipient], body, subject) + self.smtp.sendmail(helo_addr, [recipient], msg.as_string()) + + def publish(self, rset=None): + # XXX this allow anybody with access to an cubicweb application to use it as a mail relay + body = self.req.form['mailbody'] + subject = self.req.form['mailsubject'] + for recipient in self.recipients(): + text = body % recipient.as_email_context() + self.sendmail(recipient.get_email(), subject, text) + # breadcrumbs = self.req.get_session_data('breadcrumbs', None) + url = self.build_url(__message=self.req._('emails successfully sent')) + raise Redirect(url) + + +class MailBugReportController(SendMailController): + id = 'reportbug' + __selectors__ = (yes_selector,) + + def publish(self, rset=None): + body = self.req.form['description'] + self.sendmail(self.config['submit-mail'], _('%s error report') % self.config.appid, body) + url = self.build_url(__message=self.req._('bug report sent')) + raise Redirect(url) +