[migration] fix handling of default value for boolean attributes
We can't assert that the old value is 'True' or 'False', because False
used to be stored as an empty string in pre-3.18 versions.
# 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 <http://www.gnu.org/licenses/>.
"""The edit controller, automatically handling entity form submitting"""
__docformat__ = "restructuredtext en"
from warnings import warn
from collections import defaultdict
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 <edited=%r restrictions=%r kwargs=%r>' % (
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
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.iteritems():
# 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] == '1':
target_eid = values[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.set_shared_data('__maineid', form['__maineid'], txdata=True)
# 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: unicode(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, unicode(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)
try:
editedfields = formparams['_cw_entity_fields']
except KeyError:
try:
editedfields = formparams['_cw_edited_fields']
warn('[3.13] _cw_edited_fields has been renamed _cw_entity_fields',
DeprecationWarning)
except KeyError:
raise RequestError(req._('no edited fields specified for entity %s' % entity.eid))
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(), unicode(ex)) for f, ex in self.errors)
raise ValidationError(valerror_eid(entity.eid), errors)
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 '__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, iter(values).next())
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.iteritems())) )
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 = iter(redirect_info).next()
if len(eidtypes) > 1:
self._cw.set_message(self._cw._('entities deleted'))
else:
self._cw.set_message(self._cw._('entity deleted'))
def _action_apply(self):
self._default_publish()
self.reset()
def _action_cancel(self):
errorurl = self._cw.form.get('__errorurl')
if errorurl:
self._cw.cancel_edition(errorurl)
self._cw.set_message(self._cw._('edit canceled'))
return self.reset()
def _action_delete(self):
self.delete_entities(self._cw.edited_eids(withtype=True))
return self.reset()