--- /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 """<script type="text/javascript">
+ window.parent.handleFormValidationResponse('entityForm', null, %s);
+</script>""" % 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'<?xml version="1.0"?>\n' + STRICT_DOCTYPE % CW_XHTML_EXTENSIONS
+ return head + u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">%s</div>' % 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'<div id="pageContent">')
+ vtitle = self.req.form.get('vtitle')
+ if vtitle:
+ w(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+ view.pagination(req, rset, view.w, not view.need_navigation)
+ if divid == 'pageContent':
+ stream.write(u'<div id="contentmain">')
+ view.dispatch()
+ if req.form.get('paginate') and divid == 'pageContent':
+ stream.write(u'</div></div>')
+ 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 <arg> 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)
+