Commit 45f41167 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[session] cleanup hook / operation / entity edition api

Operation api
~~~~~~~~~~~~~
* commit_event killed, recently introduced postcommit_event is enough and has a better name
* kill SingleOperation class, it's a) currently never used b) superseeded by set_operation if needed.


Entity edition api
~~~~~~~~~~~~~~~~~~
edited_attributes turned into a special object holding edition specific attributes:

- attributes to be edited (simply mirrored in cw_attr_cache, actual values are there)
- former _cw_skip_security set (cw_edited) and querier_pending_relations
It has also been renamed to `cw_edited` on the way (it may also contains inlined relations)

The entity dict interface has been deprecated. One should explicitly use either
cw_attr_cache or cw_edited according to the need.

Also, there is now a control that we don't try to hi-jack edited attributes
once this has no more effect (eg modification have already been saved)

At last, _cw_set_defaults/cw_check internal methods have been moved to this
special object


Hook api
~~~~~~~~
hook.entity_oldnewvalue function now moved to a method of cw_edited object.
parent b8287e54b528
......@@ -81,6 +81,7 @@ from logilab.common.decorators import cached
from logilab.common.deprecation import deprecated
from cubicweb.server.utils import eschema_eid
from cubicweb.server.ssplanner import EditedEntity
def count_lines(stream_or_filename):
if isinstance(stream_or_filename, basestring):
......@@ -605,8 +606,7 @@ class NoHookRQLObjectStore(RQLObjectStore):
entity = copy(entity)
entity.cw_clear_relation_cache()
self.metagen.init_entity(entity)
entity.update(kwargs)
entity.edited_attributes = set(entity)
entity.cw_edited.update(kwargs, skipsec=False)
session = self.session
self.source.add_entity(session, entity)
self.source.add_info(session, entity, self.source, None, complete=False)
......@@ -679,8 +679,9 @@ class MetaGenerator(object):
entity = self.session.vreg['etypes'].etype_class(etype)(self.session)
# entity are "surface" copied, avoid shared dict between copies
del entity.cw_extra_kwargs
entity.cw_edited = EditedEntity(entity)
for attr in self.etype_attrs:
entity[attr] = self.generate(entity, attr)
entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
rels = {}
for rel in self.etype_rels:
rels[rel] = self.generate(entity, rel)
......@@ -689,7 +690,7 @@ class MetaGenerator(object):
def init_entity(self, entity):
entity.eid = self.source.create_eid(self.session)
for attr in self.entity_attrs:
entity[attr] = self.generate(entity, attr)
entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
def generate(self, entity, rtype):
return getattr(self, 'gen_%s' % rtype)(entity)
......
......@@ -631,7 +631,7 @@ class Connection(object):
else:
from cubicweb.entity import Entity
user = Entity(req, rset, row=0)
user['login'] = login # cache login
user.cw_attr_cache['login'] = login # cache login
return user
def __del__(self):
......
......@@ -19,7 +19,6 @@
__docformat__ = "restructuredtext en"
from copy import copy
from warnings import warn
from logilab.common import interface
......@@ -312,6 +311,9 @@ class Entity(AppObject):
return '<Entity %s %s %s at %s>' % (
self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
def __cmp__(self, other):
raise NotImplementedError('comparison not implemented for %s' % self.__class__)
def __json_encode__(self):
"""custom json dumps hook to dump the entity's eid
which is not part of dict structure itself
......@@ -320,107 +322,6 @@ class Entity(AppObject):
dumpable['eid'] = self.eid
return dumpable
def __nonzero__(self):
return True
def __hash__(self):
return id(self)
def __cmp__(self, other):
raise NotImplementedError('comparison not implemented for %s' % self.__class__)
def __contains__(self, key):
return key in self.cw_attr_cache
def __iter__(self):
return iter(self.cw_attr_cache)
def __getitem__(self, key):
if key == 'eid':
warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
DeprecationWarning, stacklevel=2)
return self.eid
return self.cw_attr_cache[key]
def __setitem__(self, attr, value):
"""override __setitem__ to update self.edited_attributes.
Typically, a before_[update|add]_hook could do::
entity['generated_attr'] = generated_value
and this way, edited_attributes will be updated accordingly. Also, add
the attribute to skip_security since we don't want to check security
for such attributes set by hooks.
"""
if attr == 'eid':
warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
DeprecationWarning, stacklevel=2)
self.eid = value
else:
self.cw_attr_cache[attr] = value
# don't add attribute into skip_security if already in edited
# attributes, else we may accidentaly skip a desired security check
if hasattr(self, 'edited_attributes') and \
attr not in self.edited_attributes:
self.edited_attributes.add(attr)
self._cw_skip_security_attributes.add(attr)
def __delitem__(self, attr):
"""override __delitem__ to update self.edited_attributes on cleanup of
undesired changes introduced in the entity's dict. For example, see the
code snippet below from the `forge` cube:
.. sourcecode:: python
edited = self.entity.edited_attributes
has_load_left = 'load_left' in edited
if 'load' in edited and self.entity.load_left is None:
self.entity.load_left = self.entity['load']
elif not has_load_left and edited:
# cleanup, this may cause undesired changes
del self.entity['load_left']
"""
del self.cw_attr_cache[attr]
if hasattr(self, 'edited_attributes'):
self.edited_attributes.remove(attr)
def clear(self):
self.cw_attr_cache.clear()
def get(self, key, default=None):
return self.cw_attr_cache.get(key, default)
def setdefault(self, attr, default):
"""override setdefault to update self.edited_attributes"""
value = self.cw_attr_cache.setdefault(attr, default)
# don't add attribute into skip_security if already in edited
# attributes, else we may accidentaly skip a desired security check
if hasattr(self, 'edited_attributes') and \
attr not in self.edited_attributes:
self.edited_attributes.add(attr)
self._cw_skip_security_attributes.add(attr)
return value
def pop(self, attr, default=_marker):
"""override pop to update self.edited_attributes on cleanup of
undesired changes introduced in the entity's dict. See `__delitem__`
"""
if default is _marker:
value = self.cw_attr_cache.pop(attr)
else:
value = self.cw_attr_cache.pop(attr, default)
if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
self.edited_attributes.remove(attr)
return value
def update(self, values):
"""override update to update self.edited_attributes. See `__setitem__`
"""
for attr, value in values.items():
self[attr] = value # use self.__setitem__ implementation
def cw_adapt_to(self, interface):
"""return an adapter the entity to the given interface name.
......@@ -590,12 +491,6 @@ class Entity(AppObject):
# entity cloning ##########################################################
def cw_copy(self):
thecopy = copy(self)
thecopy.cw_attr_cache = copy(self.cw_attr_cache)
thecopy._cw_related_cache = {}
return thecopy
def copy_relations(self, ceid): # XXX cw_copy_relations
"""copy relations of the object with the given eid on this
object (this method is called on the newly created copy, and
......@@ -680,7 +575,7 @@ class Entity(AppObject):
rdef = rschema.rdef(self.e_schema, attrschema)
if not self._cw.user.matching_groups(rdef.get_groups('read')) \
or (attrschema.type == 'Password' and skip_pwd):
self[attr] = None
self.cw_attr_cache[attr] = None
continue
yield attr
......@@ -739,7 +634,7 @@ class Entity(AppObject):
rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
# handle attributes
for i in xrange(1, lastattr):
self[str(selected[i-1][0])] = rset[i]
self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
# handle relations
for i in xrange(lastattr, len(rset)):
rtype, role = selected[i-1][0]
......@@ -759,7 +654,7 @@ class Entity(AppObject):
:param name: name of the attribute to get
"""
try:
value = self.cw_attr_cache[name]
return self.cw_attr_cache[name]
except KeyError:
if not self.cw_is_saved():
return None
......@@ -767,21 +662,20 @@ class Entity(AppObject):
try:
rset = self._cw.execute(rql, {'x': self.eid})
except Unauthorized:
self[name] = value = None
self.cw_attr_cache[name] = value = None
else:
assert rset.rowcount <= 1, (self, rql, rset.rowcount)
try:
self[name] = value = rset.rows[0][0]
self.cw_attr_cache[name] = value = rset.rows[0][0]
except IndexError:
# probably a multisource error
self.critical("can't get value for attribute %s of entity with eid %s",
name, self.eid)
if self.e_schema.destination(name) == 'String':
# XXX (syt) imo emtpy string is better
self[name] = value = self._cw._('unaccessible')
self.cw_attr_cache[name] = value = self._cw._('unaccessible')
else:
self[name] = value = None
return value
self.cw_attr_cache[name] = value = None
return value
def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
"""returns a resultset of related entities
......@@ -985,7 +879,6 @@ class Entity(AppObject):
you should override this method to clear them as well.
"""
# clear attributes cache
haseid = 'eid' in self
self._cw_completed = False
self.cw_attr_cache.clear()
# clear relations cache
......@@ -1012,9 +905,9 @@ class Entity(AppObject):
kwargs)
kwargs.pop('x')
# update current local object _after_ the rql query to avoid
# interferences between the query execution itself and the
# edited_attributes / skip_security_attributes machinery
self.update(kwargs)
# interferences between the query execution itself and the cw_edited /
# skip_security machinery
self.cw_attr_cache.update(kwargs)
def set_relations(self, **kwargs): # XXX cw_set_relations
"""add relations to the given object. To set a relation where this entity
......@@ -1045,58 +938,13 @@ class Entity(AppObject):
self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
{'x': self.eid}, **kwargs)
# server side utilities ###################################################
def _cw_rql_set_value(self, attr, value):
"""call by rql execution plan when some attribute is modified
don't use dict api in such case since we don't want attribute to be
added to skip_security_attributes.
This method is for internal use, you should not use it.
"""
self.cw_attr_cache[attr] = value
# server side utilities ####################################################
def _cw_clear_local_perm_cache(self, action):
for rqlexpr in self.e_schema.get_rqlexprs(action):
self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
@property
def _cw_skip_security_attributes(self):
try:
return self.__cw_skip_security_attributes
except:
self.__cw_skip_security_attributes = set()
return self.__cw_skip_security_attributes
def _cw_set_defaults(self):
"""set default values according to the schema"""
for attr, value in self.e_schema.defaults():
if not self.cw_attr_cache.has_key(attr):
self[str(attr)] = value
def _cw_check(self, creation=False):
"""check this entity against its schema. Only final relation
are checked here, constraint on actual relations are checked in hooks
"""
# necessary since eid is handled specifically and yams require it to be
# in the dictionary
if self._cw is None:
_ = unicode
else:
_ = self._cw._
if creation:
# on creations, we want to check all relations, especially
# required attributes
relations = [rschema for rschema in self.e_schema.subject_relations()
if rschema.final and rschema.type != 'eid']
elif hasattr(self, 'edited_attributes'):
relations = [self._cw.vreg.schema.rschema(rtype)
for rtype in self.edited_attributes]
else:
relations = None
self.e_schema.check(self, creation=creation, _=_,
relations=relations)
# deprecated stuff #########################################################
@deprecated('[3.9] use entity.cw_attr_value(attr)')
def get_value(self, name):
......@@ -1126,6 +974,109 @@ class Entity(AppObject):
def related_rql(self, rtype, role='subject', targettypes=None):
return self.cw_related_rql(rtype, role, targettypes)
@property
@deprecated('[3.10] use entity.cw_edited')
def edited_attributes(self):
return self.cw_edited
@property
@deprecated('[3.10] use entity.cw_edited.skip_security')
def skip_security_attributes(self):
return self.cw_edited.skip_security
@property
@deprecated('[3.10] use entity.cw_edited.skip_security')
def _cw_skip_security_attributes(self):
return self.cw_edited.skip_security
@property
@deprecated('[3.10] use entity.cw_edited.skip_security')
def querier_pending_relations(self):
return self.cw_edited.querier_pending_relations
@deprecated('[3.10] use key in entity.cw_attr_cache')
def __contains__(self, key):
return key in self.cw_attr_cache
@deprecated('[3.10] iter on entity.cw_attr_cache')
def __iter__(self):
return iter(self.cw_attr_cache)
@deprecated('[3.10] use entity.cw_attr_cache[attr]')
def __getitem__(self, key):
if key == 'eid':
warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
DeprecationWarning, stacklevel=2)
return self.eid
return self.cw_attr_cache[key]
@deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
def get(self, key, default=None):
return self.cw_attr_cache.get(key, default)
@deprecated('[3.10] use entity.cw_attr_cache.clear()')
def clear(self):
self.cw_attr_cache.clear()
# XXX clear cw_edited ?
@deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
def __setitem__(self, attr, value):
"""override __setitem__ to update self.cw_edited.
Typically, a before_[update|add]_hook could do::
entity['generated_attr'] = generated_value
and this way, cw_edited will be updated accordingly. Also, add
the attribute to skip_security since we don't want to check security
for such attributes set by hooks.
"""
if attr == 'eid':
warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
DeprecationWarning, stacklevel=2)
self.eid = value
else:
try:
self.cw_edited[attr] = value
except AttributeError:
self.cw_attr_cache[attr] = value
@deprecated('[3.10] use del entity.cw_edited[attr]')
def __delitem__(self, attr):
"""override __delitem__ to update self.cw_edited on cleanup of
undesired changes introduced in the entity's dict. For example, see the
code snippet below from the `forge` cube:
.. sourcecode:: python
edited = self.entity.cw_edited
has_load_left = 'load_left' in edited
if 'load' in edited and self.entity.load_left is None:
self.entity.load_left = self.entity['load']
elif not has_load_left and edited:
# cleanup, this may cause undesired changes
del self.entity['load_left']
"""
del self.cw_edited[attr]
@deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
def setdefault(self, attr, default):
"""override setdefault to update self.cw_edited"""
return self.cw_edited.setdefault(attr, default)
@deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
def pop(self, attr, *args):
"""override pop to update self.cw_edited on cleanup of
undesired changes introduced in the entity's dict. See `__delitem__`
"""
return self.cw_edited.pop(attr, *args)
@deprecated('[3.10] use entity.cw_edited.update(values)')
def update(self, values):
"""override update to update self.cw_edited. See `__setitem__`
"""
self.cw_edited.update(values)
# attribute and relation descriptors ##########################################
......@@ -1141,8 +1092,9 @@ class Attribute(object):
return self
return eobj.cw_attr_value(self._attrname)
@deprecated('[3.10] use entity.cw_attr_cache[attr] = value')
def __set__(self, eobj, value):
eobj[self._attrname] = value
eobj.cw_attr_cache[self._attrname] = value
class Relation(object):
......
......@@ -118,7 +118,7 @@ class DatastorePutOp(SingleOperation):
Put(gaeentity)
modified.clear()
def commit_event(self):
def postcommit_event(self):
self._put_entities()
def precommit_event(self):
......
......@@ -17,8 +17,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Core hooks: check for data integrity according to the instance'schema
validity
"""
__docformat__ = "restructuredtext en"
from threading import Lock
......@@ -64,8 +64,6 @@ def _release_unique_cstr_lock(session):
_UNIQUE_CONSTRAINTS_LOCK.release()
class _ReleaseUniqueConstraintsOperation(hook.Operation):
def commit_event(self):
pass
def postcommit_event(self):
_release_unique_cstr_lock(self.session)
def rollback_event(self):
......@@ -185,9 +183,6 @@ class _CheckConstraintsOp(hook.LateOperation):
self.critical('can\'t check constraint %s, not supported',
constraint)
def commit_event(self):
pass
class CheckConstraintHook(IntegrityHook):
"""check the relation satisfy its constraints
......@@ -219,7 +214,7 @@ class CheckAttributeConstraintHook(IntegrityHook):
def __call__(self):
eschema = self.entity.e_schema
for attr in self.entity.edited_attributes:
for attr in self.entity.cw_edited:
if eschema.subjrels[attr].final:
constraints = [c for c in eschema.rdef(attr).constraints
if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
......@@ -236,9 +231,8 @@ class CheckUniqueHook(IntegrityHook):
def __call__(self):
entity = self.entity
eschema = entity.e_schema
for attr in entity.edited_attributes:
for attr, val in entity.cw_edited.iteritems():
if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
val = entity[attr]
if val is None:
continue
rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
......@@ -257,18 +251,17 @@ class DontRemoveOwnersGroupHook(IntegrityHook):
events = ('before_delete_entity', 'before_update_entity')
def __call__(self):
if self.event == 'before_delete_entity' and self.entity.name == 'owners':
entity = self.entity
if self.event == 'before_delete_entity' and entity.name == 'owners':
msg = self._cw._('can\'t be deleted')
raise ValidationError(self.entity.eid, {None: msg})
elif self.event == 'before_update_entity' and \
'name' in self.entity.edited_attributes:
newname = self.entity.pop('name')
oldname = self.entity.name
raise ValidationError(entity.eid, {None: msg})
elif self.event == 'before_update_entity' \
and 'name' in entity.cw_edited:
oldname, newname = entity.cw_edited.oldnewvalue('name')
if oldname == 'owners' and newname != oldname:
qname = role_name('name', 'subject')
msg = self._cw._('can\'t be changed')
raise ValidationError(self.entity.eid, {qname: msg})
self.entity['name'] = newname
raise ValidationError(entity.eid, {qname: msg})
class TidyHtmlFields(IntegrityHook):
......@@ -279,15 +272,16 @@ class TidyHtmlFields(IntegrityHook):
def __call__(self):
entity = self.entity
metaattrs = entity.e_schema.meta_attributes()
edited = entity.cw_edited
for metaattr, (metadata, attr) in metaattrs.iteritems():
if metadata == 'format' and attr in entity.edited_attributes:
if metadata == 'format' and attr in edited:
try:
value = entity[attr]
value = edited[attr]
except KeyError:
continue # no text to tidy
if isinstance(value, unicode): # filter out None and Binary
if getattr(entity, str(metaattr)) == 'text/html':
entity[attr] = soup2xhtml(value, self._cw.encoding)
edited[attr] = soup2xhtml(value, self._cw.encoding)
class StripCWUserLoginHook(IntegrityHook):
......@@ -297,9 +291,9 @@ class StripCWUserLoginHook(IntegrityHook):
events = ('before_add_entity', 'before_update_entity',)
def __call__(self):
user = self.entity
if 'login' in user.edited_attributes and user.login:
user.login = user.login.strip()
login = self.entity.cw_edited.get('login')
if login:
self.entity.cw_edited['login'] = login.strip()
# 'active' integrity hooks: you usually don't want to deactivate them, they are
......
......@@ -41,11 +41,12 @@ class InitMetaAttrsHook(MetaDataHook):
def __call__(self):
timestamp = datetime.now()
self.entity.setdefault('creation_date', timestamp)
self.entity.setdefault('modification_date', timestamp)
edited = self.entity.cw_edited
edited.setdefault('creation_date', timestamp)
edited.setdefault('modification_date', timestamp)
if not self._cw.get_shared_data('do-not-insert-cwuri'):
cwuri = u'%seid/%s' % (self._cw.base_url(), self.entity.eid)
self.entity.setdefault('cwuri', cwuri)
edited.setdefault('cwuri', cwuri)
class UpdateMetaAttrsHook(MetaDataHook):
......@@ -60,7 +61,7 @@ class UpdateMetaAttrsHook(MetaDataHook):
# XXX to be really clean, we should turn off modification_date update
# explicitly on each command where we do not want that behaviour.
if not self._cw.vreg.config.repairing:
self.entity.setdefault('modification_date', datetime.now())
self.entity.cw_edited.setdefault('modification_date', datetime.now())
class _SetCreatorOp(hook.Operation):
......
......@@ -125,7 +125,7 @@ class EntityUpdateHook(NotificationHook):
if session.added_in_transaction(self.entity.eid):
return # entity is being created
# then compute changes
attrs = [k for k in self.entity.edited_attributes
attrs = [k for k in self.entity.cw_edited
if not k in self.skip_attrs]
if not attrs:
return
......@@ -168,8 +168,9 @@ class SomethingChangedHook(NotificationHook):
if self._cw.added_in_transaction(self.entity.eid):
return False
if self.entity.e_schema == 'CWUser':
if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
'last_login_time'))):
if not (frozenset(self.entity.cw_edited)
- frozenset(('eid', 'modification_date',
'last_login_time'))):
# don't record last_login_time update which are done
# automatically at login time
return False
......
......@@ -31,12 +31,9 @@ def check_entity_attributes(session, entity, editedattrs=None, creation=False):
eschema = entity.e_schema
# ._cw_skip_security_attributes is there to bypass security for attributes
# set by hooks by modifying the entity's dictionnary
dontcheck = entity._cw_skip_security_attributes
if editedattrs is None:
try:
editedattrs = entity.edited_attributes
except AttributeError:
editedattrs = entity # XXX unexpected
editedattrs = entity.cw_edited
dontcheck = editedattrs.skip_security
for attr in editedattrs:
if attr in dontcheck:
continue
......@@ -46,10 +43,6 @@ def check_entity_attributes(session, entity, editedattrs=None, creation=False):
if creation and not rdef.permissions.get('update'):
continue
rdef.check_perm(session, 'update', eid=eid)