web/views/basecontrollers.py
author Sylvain Thenault <sylvain.thenault@logilab.fr>
Mon, 05 Jan 2009 14:38:45 +0100
changeset 323 30c331fb17f0
parent 237 3df2e0ae2eba
child 353 376f9a4979e7
permissions -rw-r--r--
cleanup

# -*- 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
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,)

    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)