diff -r 058bb3dc685f -r 0b59724cb3f2 web/views/editcontroller.py --- a/web/views/editcontroller.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,395 +0,0 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""The edit controller, automatically handling entity form submitting""" - -__docformat__ = "restructuredtext en" - -from warnings import warn -from collections import defaultdict - -from datetime import datetime - -from six import text_type - -from logilab.common.deprecation import deprecated -from logilab.common.graph import ordered_nodes - -from rql.utils import rqlvar_maker - -from cubicweb import _, Binary, ValidationError -from cubicweb.view import EntityAdapter -from cubicweb.predicates import is_instance -from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, - ProcessFormError) -from cubicweb.web.views import basecontrollers, autoform - - -class IEditControlAdapter(EntityAdapter): - __regid__ = 'IEditControl' - __select__ = is_instance('Any') - - def __init__(self, _cw, **kwargs): - if self.__class__ is not IEditControlAdapter: - warn('[3.14] IEditControlAdapter is deprecated, override EditController' - ' using match_edited_type or match_form_id selectors for example.', - DeprecationWarning) - super(IEditControlAdapter, self).__init__(_cw, **kwargs) - - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - parent = self.entity.cw_adapt_to('IBreadCrumbs').parent_entity() - if parent is not None: - return parent.rest_path(), {} - return str(self.entity.e_schema).lower(), {} - - def pre_web_edit(self): - """callback called by the web editcontroller when an entity will be - created/modified, to let a chance to do some entity specific stuff. - - Do nothing by default. - """ - pass - - -def valerror_eid(eid): - try: - return int(eid) - except (ValueError, TypeError): - return eid - -class RqlQuery(object): - def __init__(self): - self.edited = [] - self.restrictions = [] - self.kwargs = {} - - def __repr__(self): - return ('Query ' % ( - 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 = next(varmaker) - while var in self.kwargs: - var = next(varmaker) - 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 - - def set_attribute(self, attr, value): - self.kwargs[attr] = value - self.edited.append('X %s %%(%s)s' % (attr, attr)) - - def set_inlined(self, relation, value): - self.kwargs[relation] = value - self.edited.append('X %s %s' % (relation, relation.upper())) - self.restrictions.append('%s eid %%(%s)s' % (relation.upper(), relation)) - - -class EditController(basecontrollers.ViewController): - __regid__ = 'edit' - - def publish(self, rset=None): - """edit / create / copy / delete entity / relations""" - for key in self._cw.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._cw._('invalid action %r' % key)) - else: - return callback() - self._default_publish() - self.reset() - - def _ordered_formparams(self): - """ Return form parameters dictionaries for each edited entity. - - We ensure that entities can be created in this order accounting for - mandatory inlined relations. - """ - req = self._cw - graph = {} - get_rschema = self._cw.vreg.schema.rschema - # minparams = 2, because at least __type and eid are needed - values_by_eid = dict((eid, req.extract_entity_params(eid, minparams=2)) - for eid in req.edited_eids()) - # iterate over all the edited entities - for eid, values in values_by_eid.items(): - # add eid to the dependency graph - graph.setdefault(eid, set()) - # search entity's edited fields for mandatory inlined relation - for param in values['_cw_entity_fields'].split(','): - try: - rtype, role = param.split('-') - except ValueError: - # e.g. param='__type' - continue - rschema = get_rschema(rtype) - if rschema.inlined: - for target in rschema.targets(values['__type'], role): - rdef = rschema.role_rdef(values['__type'], target, role) - # if cardinality is 1 and if the target entity is being - # simultaneously edited, the current entity must be - # created before the target one - if rdef.cardinality[0 if role == 'subject' else 1] == '1': - # use .get since param may be unspecified (though it will usually lead - # to a validation error later) - target_eid = values.get(param) - if target_eid in values_by_eid: - # add dependency from the target entity to the - # current one - if role == 'object': - graph.setdefault(target_eid, set()).add(eid) - else: - graph.setdefault(eid, set()).add(target_eid) - break - for eid in reversed(ordered_nodes(graph)): - yield values_by_eid[eid] - - def _default_publish(self): - req = self._cw - self.errors = [] - self.relations_rql = [] - form = req.form - # so we're able to know the main entity from the repository side - if '__maineid' in form: - req.transaction_data['__maineid'] = form['__maineid'] - # no specific action, generic edition - self._to_create = req.data['eidmap'] = {} - # those two data variables are used to handle relation from/to entities - # which doesn't exist at time where the entity is edited and that - # deserves special treatment - req.data['pending_inlined'] = defaultdict(set) - req.data['pending_others'] = set() - try: - for formparams in self._ordered_formparams(): - eid = self.edit_entity(formparams) - except (RequestError, NothingToEdit) as ex: - if '__linkto' in req.form and 'eid' in req.form: - self.execute_linkto() - elif not ('__delete' in req.form or '__insert' in req.form): - raise ValidationError(None, {None: text_type(ex)}) - # all pending inlined relations to newly created entities have been - # treated now (pop to ensure there are no attempt to add new ones) - pending_inlined = req.data.pop('pending_inlined') - assert not pending_inlined, pending_inlined - # handle all other remaining relations now - for form_, field in req.data.pop('pending_others'): - self.handle_formfield(form_, field) - # then execute rql to set all relations - for querydef in self.relations_rql: - self._cw.execute(*querydef) - # XXX this processes *all* pending operations of *all* entities - if '__delete' in req.form: - todelete = req.list_form_param('__delete', req.form, pop=True) - if todelete: - autoform.delete_relations(self._cw, todelete) - self._cw.remove_pending_operations() - if self.errors: - errors = dict((f.name, text_type(ex)) for f, ex in self.errors) - raise ValidationError(valerror_eid(form.get('__maineid')), errors) - - def _insert_entity(self, etype, eid, rqlquery): - rql = rqlquery.insert_query(etype) - try: - entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0) - neweid = entity.eid - except ValidationError as ex: - self._to_create[eid] = ex.entity - if self._cw.ajax_request: # XXX (syt) why? - ex.entity = eid - raise - self._to_create[eid] = neweid - return neweid - - def _update_entity(self, eid, rqlquery): - self._cw.execute(rqlquery.update_query(eid), rqlquery.kwargs) - - def edit_entity(self, formparams, multiple=False): - """edit / create / copy an entity and return its eid""" - req = self._cw - etype = formparams['__type'] - entity = req.vreg['etypes'].etype_class(etype)(req) - entity.eid = valerror_eid(formparams['eid']) - is_main_entity = req.form.get('__maineid') == formparams['eid'] - # let a chance to do some entity specific stuff - entity.cw_adapt_to('IEditControl').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 - if is_main_entity: - formid = req.form.get('__form_id', 'edition') - else: - # XXX inlined forms formid should be saved in a different formparams entry - # inbetween, use cubicweb standard formid for inlined forms - formid = 'edition' - form = req.vreg['forms'].select(formid, req, entity=entity) - eid = form.actual_eid(entity.eid) - editedfields = formparams['_cw_entity_fields'] - form.formvalues = {} # init fields value cache - for field in form.iter_modified_fields(editedfields, entity): - self.handle_formfield(form, field, rqlquery) - # if there are some inlined field which were waiting for this entity's - # creation, add relevant data to the rqlquery - for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): - rqlquery.set_inlined(field.name, form_.edited_entity.eid) - if self.errors: - errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) - raise ValidationError(valerror_eid(entity.eid), errors) - if eid is None: # creation or copy - entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) - elif rqlquery.edited: # edition of an existant entity - self.check_concurrent_edition(formparams, eid) - self._update_entity(eid, rqlquery) - if is_main_entity: - self.notify_edited(entity) - if '__delete' in formparams: - # XXX deprecate? - todelete = req.list_form_param('__delete', formparams, pop=True) - autoform.delete_relations(req, todelete) - if '__cloned_eid' in formparams: - entity.copy_relations(int(formparams['__cloned_eid'])) - if is_main_entity: # only execute linkto for the main entity - self.execute_linkto(entity.eid) - return eid - - def handle_formfield(self, form, field, rqlquery=None): - eschema = form.edited_entity.e_schema - try: - for field, value in field.process_posted(form): - if not ( - (field.role == 'subject' and field.name in eschema.subjrels) - or - (field.role == 'object' and field.name in eschema.objrels)): - continue - rschema = self._cw.vreg.schema.rschema(field.name) - if rschema.final: - rqlquery.set_attribute(field.name, value) - else: - if form.edited_entity.has_eid(): - origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True)) - else: - origvalues = set() - if value is None or value == origvalues: - continue # not edited / not modified / to do later - if rschema.inlined and rqlquery is not None and field.role == 'subject': - self.handle_inlined_relation(form, field, value, origvalues, rqlquery) - elif form.edited_entity.has_eid(): - self.handle_relation(form, field, value, origvalues) - else: - form._cw.data['pending_others'].add( (form, field) ) - except ProcessFormError as exc: - self.errors.append((field, exc)) - - def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): - """handle edition for the (rschema, x) relation of the given entity - """ - if values: - rqlquery.set_inlined(field.name, next(iter(values))) - elif form.edited_entity.has_eid(): - self.handle_relation(form, field, values, origvalues) - - def handle_relation(self, form, field, values, origvalues): - """handle edition for the (rschema, x) relation of the given entity - """ - etype = form.edited_entity.e_schema - rschema = self._cw.vreg.schema.rschema(field.name) - if field.role == 'subject': - desttype = rschema.objects(etype)[0] - card = rschema.rdef(etype, desttype).cardinality[0] - subjvar, objvar = 'X', 'Y' - else: - desttype = rschema.subjects(etype)[0] - card = rschema.rdef(desttype, etype).cardinality[1] - subjvar, objvar = 'Y', 'X' - eid = form.edited_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): - self.relations_rql.append((rql, {'x': eid, 'y': reid})) - 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: - self.relations_rql.append((rql, {'x': eid, 'y': reid})) - - def delete_entities(self, eidtypes): - """delete entities from the repository""" - redirect_info = set() - eidtypes = tuple(eidtypes) - for eid, etype in eidtypes: - entity = self._cw.entity_from_eid(eid, etype) - path, params = entity.cw_adapt_to('IEditControl').after_deletion_path() - redirect_info.add( (path, tuple(params.items())) ) - entity.cw_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 = next(iter(redirect_info)) - if len(eidtypes) > 1: - self._cw.set_message(self._cw._('entities deleted')) - else: - self._cw.set_message(self._cw._('entity deleted')) - - - def check_concurrent_edition(self, formparams, eid): - req = self._cw - try: - form_ts = datetime.utcfromtimestamp(float(formparams['__form_generation_time'])) - except KeyError: - # Backward and tests compatibility : if no timestamp consider edition OK - return - if req.execute("Any X WHERE X modification_date > %(fts)s, X eid %(eid)s", - {'eid': eid, 'fts': form_ts}): - # We only mark the message for translation but the actual - # translation will be handled by the Validation mechanism... - msg = _("Entity %(eid)s has changed since you started to edit it." - " Reload the page and reapply your changes.") - # ... this is why we pass the formats' dict as a third argument. - raise ValidationError(eid, {None: msg}, {'eid' : eid}) - - def _action_apply(self): - self._default_publish() - self.reset() - - def _action_delete(self): - self.delete_entities(self._cw.edited_eids(withtype=True)) - return self.reset()