web/views/editcontroller.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 23 Sep 2009 19:16:44 +0200
changeset 3445 32492e9e734b
parent 3396 fb261afd49cd
child 3451 6b46d73823f5
permissions -rw-r--r--
update and simplify test

"""The edit controller, handling form submitting.

:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

from decimal import Decimal

from rql.utils import rqlvar_maker

from cubicweb import Binary, ValidationError, typed_eid
from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
from cubicweb.web.controller import parse_relations_descr
from cubicweb.web.views.basecontrollers import ViewController


class ToDoLater(Exception):
    """exception used in the edit controller to indicate that a relation
    can't be handled right now and have to be handled later
    """

class RqlQuery(object):
    def __init__(self):
        self.edited = []
        self.restrictions = []
        self.kwargs = {}

    def insert_query(self, etype):
        if self.edited:
            rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited))
        else:
            rql = 'INSERT %s X' % etype
        if self.restrictions:
            rql += ' WHERE %s' % ','.join(self.restrictions)
        return rql

    def update_query(self, eid):
        varmaker = rqlvar_maker()
        var = varmaker.next()
        while var in self.kwargs:
            var = varmaker.next()
        rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.edited), var)
        if self.restrictions:
            rql += ', %s' % ','.join(self.restrictions)
        self.kwargs[var] = eid
        return rql

class EditController(ViewController):
    __regid__ = 'edit'

    def publish(self, rset=None):
        """edit / create / copy / delete entity / relations"""
        for key in self.req.form:
            # There should be 0 or 1 action
            if key.startswith('__action_'):
                cbname = key[1:]
                try:
                    callback = getattr(self, cbname)
                except AttributeError:
                    raise RequestError(self.req._('invalid action %r' % key))
                else:
                    return callback()
        self._default_publish()
        self.reset()

    def _default_publish(self):
        req = self.req
        self.errors = []
        self.relations_rql = []
        # no specific action, generic edition
        self._to_create = req.data['eidmap'] = {}
        self._pending_relations = []
        todelete = self.req.get_pending_deletes()
        toinsert = self.req.get_pending_inserts()
        try:
            methodname = req.form.pop('__method', None)
            for eid in req.edited_eids():
                # __type and eid
                formparams = req.extract_entity_params(eid, minparams=2)
                if methodname is not None:
                    entity = req.entity_from_eid(eid)
                    method = getattr(entity, methodname)
                    method(formparams)
                eid = self.edit_entity(formparams)
        except (RequestError, NothingToEdit):
            if '__linkto' in req.form and 'eid' in req.form:
                self.execute_linkto()
            elif not ('__delete' in req.form or '__insert' in req.form or todelete or toinsert):
                raise ValidationError(None, {None: req._('nothing to edit')})
        for querydef in self.relations_rql:
            self.req.execute(*querydef)
        # handle relations in newly created entities
        # XXX find a way to merge _pending_relations and relations_rql
        if self._pending_relations:
            for form, field, entity in self._pending_relations:
                for querydef in self.handle_relation(form, field, entity, True):
                    self.req.execute(*querydef)
        # XXX this processes *all* pending operations of *all* entities
        if req.form.has_key('__delete'):
            todelete += req.list_form_param('__delete', req.form, pop=True)
        if todelete:
            self.delete_relations(parse_relations_descr(todelete))
        if req.form.has_key('__insert'):
            toinsert = req.list_form_param('__insert', req.form, pop=True)
        if toinsert:
            self.insert_relations(parse_relations_descr(toinsert))
        self.req.remove_pending_operations()

    def _insert_entity(self, etype, eid, rqlquery):
        rql = rqlquery.insert_query(etype)
        try:
            # get the new entity (in some cases, the type might have
            # changed as for the File --> Image mutation)
            entity = self.req.execute(rql, rqlquery.kwargs).get_entity(0, 0)
            neweid = entity.eid
        except ValidationError, ex:
            self._to_create[eid] = ex.entity
            if self.req.json_request: # XXX (syt) why?
                ex.entity = eid
            raise
        self._to_create[eid] = neweid
        return neweid

    def _update_entity(self, eid, rqlquery):
        rql = rqlquery.update_query(eid)
        self.req.execute(rql, rqlquery.kwargs)

    def edit_entity(self, formparams, multiple=False):
        """edit / create / copy an entity and return its eid"""
        etype = formparams['__type']
        entity = self.vreg['etypes'].etype_class(etype)(self.req)
        entity.eid = formparams['eid']
        eid = self._get_eid(entity.eid)
        is_main_entity = self.req.form.get('__maineid') == formparams['eid']
        # let a chance to do some entity specific stuff.tn
        entity.pre_web_edit()
        # create a rql query from parameters
        rqlquery = RqlQuery()
        # process inlined relations at the same time as attributes
        # this will generate less rql queries and might be useful in
        # a few dark corners
        formid = self.req.form.get('__form_id', 'edition')
        form = self.vreg['forms'].select(formid, self.req, entity=entity)
        for field in form.fields:
            if form.form_field_modified(field):
                self.handle_formfield(form, field, entity, rqlquery)
        if eid is None: # creation or copy
            entity.eid = self._insert_entity(etype, formparams['eid'], rqlquery)
        elif rqlquery.edited: # edition of an existant entity
            self._update_entity(eid, rqlquery)
        if is_main_entity:
            self.notify_edited(entity)
        if formparams.has_key('__delete'):
            todelete = self.req.list_form_param('__delete', formparams, pop=True)
            self.delete_relations(parse_relations_descr(todelete))
        if formparams.has_key('__cloned_eid'):
            entity.copy_relations(formparams['__cloned_eid'])
        if formparams.has_key('__insert'):
            toinsert = self.req.list_form_param('__insert', formparams, pop=True)
            self.insert_relations(parse_relations_descr(toinsert))
        if is_main_entity: # only execute linkto for the main entity
            self.execute_linkto(eid)
        return eid

    def handle_formfield(self, form, field, entity, rqlquery):
        eschema = entity.e_schema
        try:
            for attr, value in field.process_posted(form):
                if not (
                    (field.role == 'subject' and eschema.has_subject_relation(field.name))
                    or
                    (field.role == 'object' and eschema.has_object_relation(field.name))):
                    continue
                rschema = self.schema.rschema(field.name)
                if rschema.is_final():
                    rqlquery.kwargs[attr] = value
                    rqlquery.edited.append('X %s %%(%s)s' % (attr, attr))
                elif rschema.inlined:
                    self.handle_inlined_relation(form, field, entity, rqlquery)
                else:
                    self.relations_rql += self.handle_relation(
                        form, field, entity)
        except ProcessFormError, exc:
            self.errors.append((field, exc))

    def _action_apply(self):
        self._default_publish()
        self.reset()

    def _action_cancel(self):
        errorurl = self.req.form.get('__errorurl')
        if errorurl:
            self.req.cancel_edition(errorurl)
        self.req.message = self.req._('edit canceled')
        return self.reset()

    def _action_delete(self):
        self.delete_entities(self.req.edited_eids(withtype=True))
        return self.reset()

    def _relation_values(self, form, field, entity, late=False):
        """handle edition for the (rschema, x) relation of the given entity
        """
        values = set()
        for eid in field.process_form_data(form):
            if not eid: # AutoCompletionWidget
                continue
            typed_eid = self._get_eid(eid)
            if typed_eid is None:
                if late:
                    # eid is still None while it's already a late call
                    # this mean that the associated entity has not been created
                    raise Exception("eid %s is still not created" % eid)
                self._pending_relations.append( (form, field, entity) )
                return None
            values.add(typed_eid)
        return values

    def handle_inlined_relation(self, form, field, entity, rqlquery):
        """handle edition for the (rschema, x) relation of the given entity
        """
        origvalues = set(row[0] for row in entity.related(field.name, field.role))
        values  = self._relation_values(form, field, entity)
        if values is None or values == origvalues:
            return # not edited / not modified / to do later
        attr = field.name
        if values:
            rqlquery.kwargs[attr] = iter(values).next()
            rqlquery.edition.append('X %s %s' % (attr, attr.upper()))
            rqlquery.restrictions.append('%s eid %%(%s)s' % (attr.upper(), attr))
        elif entity.has_eid():
            self.relations_rql += self.handle_relation(form, field, entity)

    def handle_relation(self, form, field, entity, late=False):
        """handle edition for the (rschema, x) relation of the given entity
        """
        origvalues = set(row[0] for row in entity.related(field.name, field.role))
        values  = self._relation_values(form, field, entity, late)
        if values is None or values == origvalues:
            return # not edited / not modified / to do later
        etype = entity.e_schema
        rschema = self.schema.rschema(field.name)
        if field.role == 'subject':
            desttype = rschema.objects(etype)[0]
            card = rschema.rproperty(etype, desttype, 'cardinality')[0]
            subjvar, objvar = 'X', 'Y'
        else:
            desttype = rschema.subjects(etype)[0]
            card = rschema.rproperty(desttype, etype, 'cardinality')[1]
            subjvar, objvar = 'Y', 'X'
        eid = entity.eid
        if field.role == 'object' or not rschema.inlined or not values:
            # this is not an inlined relation or no values specified,
            # explicty remove relations
            rql = 'DELETE %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % (
                subjvar, rschema, objvar)
            for reid in origvalues.difference(values):
                yield (rql, {'x': eid, 'y': reid}, ('x', 'y'))
        seteids = values.difference(origvalues)
        if seteids:
            rql = 'SET %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % (
                subjvar, rschema, objvar)
            for reid in seteids:
                yield (rql, {'x': eid, 'y': reid}, ('x', 'y'))

    def _get_eid(self, eid):
        # should be either an int (existant entity) or a variable (to be
        # created entity)
        assert eid or eid == 0, repr(eid) # 0 is a valid eid
        try:
            return typed_eid(eid)
        except ValueError:
            try:
                return self._to_create[eid]
            except KeyError:
                self._to_create[eid] = None
                return None

    def _linked_eids(self, eids, late=False):
        """return a list of eids if they are all known, else raise ToDoLater
        """