# HG changeset patch # User Sylvain Thénault # Date 1282724958 -7200 # Node ID 8bc6eac1fac1d40ed72f52889271df67702ee55e # Parent b8287e54b528316fc08ab92394e82844af7aa36e [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. diff -r b8287e54b528 -r 8bc6eac1fac1 dataimport.py --- a/dataimport.py Wed Aug 25 10:29:07 2010 +0200 +++ b/dataimport.py Wed Aug 25 10:29:18 2010 +0200 @@ -81,6 +81,7 @@ 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 @@ 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 @@ 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 @@ 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) diff -r b8287e54b528 -r 8bc6eac1fac1 dbapi.py --- a/dbapi.py Wed Aug 25 10:29:07 2010 +0200 +++ b/dbapi.py Wed Aug 25 10:29:18 2010 +0200 @@ -631,7 +631,7 @@ 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): diff -r b8287e54b528 -r 8bc6eac1fac1 entity.py --- a/entity.py Wed Aug 25 10:29:07 2010 +0200 +++ b/entity.py Wed Aug 25 10:29:18 2010 +0200 @@ -19,7 +19,6 @@ __docformat__ = "restructuredtext en" -from copy import copy from warnings import warn from logilab.common import interface @@ -312,6 +311,9 @@ return '' % ( 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ :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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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): diff -r b8287e54b528 -r 8bc6eac1fac1 goa/gaesource.py --- a/goa/gaesource.py Wed Aug 25 10:29:07 2010 +0200 +++ b/goa/gaesource.py Wed Aug 25 10:29:18 2010 +0200 @@ -118,7 +118,7 @@ Put(gaeentity) modified.clear() - def commit_event(self): + def postcommit_event(self): self._put_entities() def precommit_event(self): diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/integrity.py --- a/hooks/integrity.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/integrity.py Wed Aug 25 10:29:18 2010 +0200 @@ -17,8 +17,8 @@ # with CubicWeb. If not, see . """Core hooks: check for data integrity according to the instance'schema validity +""" -""" __docformat__ = "restructuredtext en" from threading import Lock @@ -64,8 +64,6 @@ _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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/metadata.py --- a/hooks/metadata.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/metadata.py Wed Aug 25 10:29:18 2010 +0200 @@ -41,11 +41,12 @@ 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 @@ # 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): diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/notification.py --- a/hooks/notification.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/notification.py Wed Aug 25 10:29:18 2010 +0200 @@ -125,7 +125,7 @@ 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 @@ 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 diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/security.py --- a/hooks/security.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/security.py Wed Aug 25 10:29:18 2010 +0200 @@ -31,12 +31,9 @@ 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 @@ if creation and not rdef.permissions.get('update'): continue rdef.check_perm(session, 'update', eid=eid) - # don't update dontcheck until everything went fine: see usage in - # after_update_entity, where if we got an Unauthorized at hook time, we will - # retry and commit time - dontcheck |= frozenset(editedattrs) class _CheckEntityPermissionOp(hook.LateOperation): @@ -57,15 +50,12 @@ #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action session = self.session for values in session.transaction_data.pop('check_entity_perm_op'): - entity = session.entity_from_eid(values[0]) - action = values[1] + eid, action, edited = values + entity = session.entity_from_eid(eid) entity.cw_check_perm(action) - check_entity_attributes(session, entity, values[2:], + check_entity_attributes(session, entity, edited, creation=self.creation) - def commit_event(self): - pass - class _CheckRelationPermissionOp(hook.LateOperation): def precommit_event(self): @@ -76,9 +66,6 @@ session.describe(eidto)[0]) rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto) - def commit_event(self): - pass - @objectify_selector @lltrace @@ -99,7 +86,7 @@ def __call__(self): hook.set_operation(self._cw, 'check_entity_perm_op', - (self.entity.eid, 'add') + tuple(self.entity.edited_attributes), + (self.entity.eid, 'add', self.entity.cw_edited), _CheckEntityPermissionOp, creation=True) @@ -115,10 +102,10 @@ except Unauthorized: self.entity._cw_clear_local_perm_cache('update') # save back editedattrs in case the entity is reedited later in the - # same transaction, which will lead to edited_attributes being + # same transaction, which will lead to cw_edited being # overwritten hook.set_operation(self._cw, 'check_entity_perm_op', - (self.entity.eid, 'update') + tuple(self.entity.edited_attributes), + (self.entity.eid, 'update', self.entity.cw_edited), _CheckEntityPermissionOp, creation=False) diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/syncschema.py --- a/hooks/syncschema.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/syncschema.py Wed Aug 25 10:29:18 2010 +0200 @@ -128,14 +128,12 @@ def check_valid_changes(session, entity, ro_attrs=('name', 'final')): errors = {} # don't use getattr(entity, attr), we would get the modified value if any - for attr in entity.edited_attributes: + for attr in entity.cw_edited: if attr in ro_attrs: - newval = entity.pop(attr) - origval = getattr(entity, attr) + origval, newval = entity.cw_edited.oldnewvalue(attr) if newval != origval: errors[attr] = session._("can't change the %s attribute") % \ display_name(session, attr) - entity[attr] = newval if errors: raise ValidationError(entity.eid, errors) @@ -862,7 +860,7 @@ def __call__(self): entity = self.entity - if entity.get('final'): + if entity.cw_edited.get('final'): return CWETypeAddOp(self._cw, entity=entity) @@ -876,8 +874,8 @@ entity = self.entity check_valid_changes(self._cw, entity, ro_attrs=('final',)) # don't use getattr(entity, attr), we would get the modified value if any - if 'name' in entity.edited_attributes: - oldname, newname = hook.entity_oldnewvalue(entity, 'name') + if 'name' in entity.cw_edited: + oldname, newname = entity.cw_edited.oldnewvalue('name') if newname.lower() != oldname.lower(): CWETypeRenameOp(self._cw, oldname=oldname, newname=newname) @@ -920,8 +918,8 @@ entity = self.entity rtypedef = ybo.RelationType(name=entity.name, description=entity.description, - inlined=entity.get('inlined', False), - symmetric=entity.get('symmetric', False), + inlined=entity.cw_edited.get('inlined', False), + symmetric=entity.cw_edited.get('symmetric', False), eid=entity.eid) MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef) @@ -936,10 +934,10 @@ check_valid_changes(self._cw, entity) newvalues = {} for prop in ('symmetric', 'inlined', 'fulltext_container'): - if prop in entity.edited_attributes: - old, new = hook.entity_oldnewvalue(entity, prop) + if prop in entity.cw_edited: + old, new = entity.cw_edited.oldnewvalue(prop) if old != new: - newvalues[prop] = entity[prop] + newvalues[prop] = new if newvalues: rschema = self._cw.vreg.schema.rschema(entity.name) CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity, @@ -1024,8 +1022,8 @@ attr = 'ordernum' else: attr = prop - if attr in entity.edited_attributes: - old, new = hook.entity_oldnewvalue(entity, attr) + if attr in entity.cw_edited: + old, new = entity.cw_edited.oldnewvalue(attr) if old != new: newvalues[prop] = new if newvalues: diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/syncsession.py --- a/hooks/syncsession.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/syncsession.py Wed Aug 25 10:29:18 2010 +0200 @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Core hooks: synchronize living session on persistent data changes +"""Core hooks: synchronize living session on persistent data changes""" -""" __docformat__ = "restructuredtext en" from yams.schema import role_name @@ -56,26 +55,25 @@ class _DeleteGroupOp(_GroupOperation): """synchronize user when a in_group relation has been deleted""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" groups = self.cnxuser.groups try: groups.remove(self.group) except KeyError: self.error('user %s not in group %s', self.cnxuser, self.group) - return class _AddGroupOp(_GroupOperation): """synchronize user when a in_group relation has been added""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" groups = self.cnxuser.groups if self.group in groups: self.warning('user %s already in group %s', self.cnxuser, self.group) - return - groups.add(self.group) + else: + groups.add(self.group) class SyncInGroupHook(SyncSessionHook): @@ -98,7 +96,7 @@ self.cnxid = cnxid hook.Operation.__init__(self, session) - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" try: self.session.repo.close(self.cnxid) @@ -123,7 +121,7 @@ class _DelCWPropertyOp(hook.Operation): """a user's custom properties has been deleted""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" try: del self.cwpropdict[self.key] @@ -134,7 +132,7 @@ class _ChangeCWPropertyOp(hook.Operation): """a user's custom properties has been added/changed""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" self.cwpropdict[self.key] = self.value @@ -142,7 +140,7 @@ class _AddCWPropertyOp(hook.Operation): """a user's custom properties has been added/changed""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" cwprop = self.cwprop if not cwprop.for_user: @@ -180,8 +178,8 @@ def __call__(self): entity = self.entity - if not ('pkey' in entity.edited_attributes or - 'value' in entity.edited_attributes): + if not ('pkey' in entity.cw_edited or + 'value' in entity.cw_edited): return key, value = entity.pkey, entity.value session = self._cw diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/test/unittest_hooks.py Wed Aug 25 10:29:18 2010 +0200 @@ -143,7 +143,7 @@ entity.set_attributes(name=u'wf2') self.assertEquals(entity.description, u'yo') entity.set_attributes(description=u'R&D

yo') - entity.pop('description') + entity.cw_attr_cache.pop('description') self.assertEquals(entity.description, u'R&D

yo

') diff -r b8287e54b528 -r 8bc6eac1fac1 hooks/workflow.py --- a/hooks/workflow.py Wed Aug 25 10:29:07 2010 +0200 +++ b/hooks/workflow.py Wed Aug 25 10:29:18 2010 +0200 @@ -135,7 +135,7 @@ qname = role_name('to_state', 'subject') msg = session._("state doesn't belong to entity's current workflow") raise ValidationError(self.trinfo.eid, {'to_state': msg}) - tostate = wftr.get_exit_point(forentity, trinfo['to_state']) + tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) if tostate is not None: # reached an exit point msg = session._('exiting from subworkflow %s') @@ -185,7 +185,7 @@ entity = self.entity # first retreive entity to which the state change apply try: - foreid = entity['wf_info_for'] + foreid = entity.cw_attr_cache['wf_info_for'] except KeyError: qname = role_name('wf_info_for', 'subject') msg = session._('mandatory relation') @@ -213,7 +213,7 @@ or not session.write_security) # no investigate the requested state change... try: - treid = entity['by_transition'] + treid = entity.cw_attr_cache['by_transition'] except KeyError: # no transition set, check user is a manager and destination state # is specified (and valid) @@ -221,7 +221,7 @@ qname = role_name('by_transition', 'subject') msg = session._('mandatory relation') raise ValidationError(entity.eid, {qname: msg}) - deststateeid = entity.get('to_state') + deststateeid = entity.cw_attr_cache.get('to_state') if not deststateeid: qname = role_name('by_transition', 'subject') msg = session._('mandatory relation') @@ -247,8 +247,8 @@ if not tr.may_be_fired(foreid): msg = session._("transition may not be fired") raise ValidationError(entity.eid, {qname: msg}) - if entity.get('to_state'): - deststateeid = entity['to_state'] + deststateeid = entity.cw_attr_cache.get('to_state') + if deststateeid is not None: if not cowpowers and deststateeid != tr.destination(forentity).eid: qname = role_name('by_transition', 'subject') msg = session._("transition isn't allowed") @@ -262,8 +262,8 @@ else: deststateeid = tr.destination(forentity).eid # everything is ok, add missing information on the trinfo entity - entity['from_state'] = fromstate.eid - entity['to_state'] = deststateeid + entity.cw_edited['from_state'] = fromstate.eid + entity.cw_edited['to_state'] = deststateeid nocheck = session.transaction_data.setdefault('skip-security', set()) nocheck.add((entity.eid, 'from_state', fromstate.eid)) nocheck.add((entity.eid, 'to_state', deststateeid)) @@ -278,11 +278,12 @@ def __call__(self): trinfo = self.entity - _change_state(self._cw, trinfo['wf_info_for'], - trinfo['from_state'], trinfo['to_state']) - forentity = self._cw.entity_from_eid(trinfo['wf_info_for']) + rcache = trinfo.cw_attr_cache + _change_state(self._cw, rcache['wf_info_for'], rcache['from_state'], + rcache['to_state']) + forentity = self._cw.entity_from_eid(rcache['wf_info_for']) iworkflowable = forentity.cw_adapt_to('IWorkflowable') - assert iworkflowable.current_state.eid == trinfo['to_state'] + assert iworkflowable.current_state.eid == rcache['to_state'] if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid: _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo) diff -r b8287e54b528 -r 8bc6eac1fac1 rset.py --- a/rset.py Wed Aug 25 10:29:07 2010 +0200 +++ b/rset.py Wed Aug 25 10:29:18 2010 +0200 @@ -484,7 +484,7 @@ if attr == 'eid': entity.eid = rowvalues[outerselidx] else: - entity[attr] = rowvalues[outerselidx] + entity.cw_attr_cache[attr] = rowvalues[outerselidx] continue else: rschema = eschema.objrels[attr] diff -r b8287e54b528 -r 8bc6eac1fac1 server/hook.py --- a/server/hook.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/hook.py Wed Aug 25 10:29:18 2010 +0200 @@ -61,6 +61,7 @@ from logilab.common.logging_ext import set_log_methods from cubicweb import RegistryNotFound +from cubicweb.vregistry import classid from cubicweb.cwvreg import CWRegistry, VRegistry from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, is_instance) @@ -83,7 +84,7 @@ for appobjects in self.values(): for cls in appobjects: if not cls.enabled: - warn('[3.6] %s: enabled is deprecated' % cls) + warn('[3.6] %s: enabled is deprecated' % classid(cls)) self.unregister(cls) def register(self, obj, **kwargs): @@ -119,21 +120,9 @@ for event in ALL_HOOKS: VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry -_MARKER = object() +@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)') def entity_oldnewvalue(entity, attr): - """returns the couple (old attr value, new attr value) - - NOTE: will only work in a before_update_entity hook - """ - # get new value and remove from local dict to force a db query to - # fetch old value - newvalue = entity.pop(attr, _MARKER) - oldvalue = getattr(entity, attr) - if newvalue is not _MARKER: - entity[attr] = newvalue - else: - newvalue = oldvalue - return oldvalue, newvalue + return entity.cw_edited.oldnewvalue(attr) # some hook specific selectors ################################################# @@ -231,16 +220,16 @@ @classproperty def __regid__(cls): - warn('[3.6] %s.%s: please specify an id for your hook' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: please specify an id for your hook' % classid(cls), + DeprecationWarning) return str(id(cls)) @classmethod def __registered__(cls, reg): super(Hook, cls).__registered__(reg) if getattr(cls, 'accepts', None): - warn('[3.6] %s.%s: accepts is deprecated, define proper __select__' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: accepts is deprecated, define proper __select__' + % classid(cls), DeprecationWarning) rtypes = [] for ertype in cls.accepts: if ertype.islower(): @@ -261,9 +250,8 @@ def __call__(self): if hasattr(self, 'call'): - cls = self.__class__ - warn('[3.6] %s.%s: call is deprecated, implement __call__' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: call is deprecated, implement __call__' + % classid(self.__class__), DeprecationWarning) if self.event.endswith('_relation'): self.call(self._cw, self.eidfrom, self.rtype, self.eidto) elif 'delete' in self.event: @@ -428,6 +416,10 @@ def handle_event(self, event): """delegate event handling to the opertaion""" + if event == 'postcommit_event' and hasattr(self, 'commit_event'): + warn('[3.10] %s: commit_event method has been replaced by postcommit_event' + % classid(self.__class__), DeprecationWarning) + self.commit_event() getattr(self, event)() def precommit_event(self): @@ -440,16 +432,6 @@ been all considered if it's this operation which failed """ - def commit_event(self): - """the observed connections pool is commiting""" - - def revertcommit_event(self): - """an error went when commiting this operation or a later one - - should revert commit's changes but take care, they may have not - been all considered if it's this operation which failed - """ - def rollback_event(self): """the observed connections pool has been rollbacked @@ -524,8 +506,12 @@ return -(i + 1) -class SingleOperation(Operation): - """special operation which should be called once""" + +class SingleLastOperation(Operation): + """special operation which should be called once and after all other + operations + """ + def register(self, session): """override register to handle cases where this operation has already been added @@ -546,11 +532,6 @@ return -(i+1) return None - -class SingleLastOperation(SingleOperation): - """special operation which should be called once and after all other - operations - """ def insert_index(self): return None @@ -572,7 +553,7 @@ if previous: self.to_send = previous.to_send + self.to_send - def commit_event(self): + def postcommit_event(self): self.session.repo.threaded_task(self.sendmails) def sendmails(self): @@ -612,7 +593,7 @@ type/source cache eids of entities deleted in that transaction. """ - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been rollbacked, remove inserted eid from repository type/source cache """ diff -r b8287e54b528 -r 8bc6eac1fac1 server/pool.py --- a/server/pool.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/pool.py Wed Aug 25 10:29:18 2010 +0200 @@ -144,11 +144,9 @@ self._cursors.pop(source.uri, None) -from cubicweb.server.hook import (Operation, LateOperation, SingleOperation, - SingleLastOperation) +from cubicweb.server.hook import Operation, LateOperation, SingleLastOperation from logilab.common.deprecation import class_moved, class_renamed Operation = class_moved(Operation) PreCommitOperation = class_renamed('PreCommitOperation', Operation) LateOperation = class_moved(LateOperation) -SingleOperation = class_moved(SingleOperation) SingleLastOperation = class_moved(SingleLastOperation) diff -r b8287e54b528 -r 8bc6eac1fac1 server/querier.py --- a/server/querier.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/querier.py Wed Aug 25 10:29:18 2010 +0200 @@ -450,7 +450,7 @@ # save originaly selected variable, we may modify this # dictionary for substitution (query parameters) self.selected = rqlst.selection - # list of new or updated entities definition (utils.Entity) + # list of rows of entities definition (ssplanner.EditedEntity) self.e_defs = [[]] # list of new relation definition (3-uple (from_eid, r_type, to_eid) self.r_defs = set() @@ -461,7 +461,6 @@ def add_entity_def(self, edef): """add an entity definition to build""" - edef.querier_pending_relations = {} self.e_defs[-1].append(edef) def add_relation_def(self, rdef): @@ -493,8 +492,9 @@ self.e_defs[i][colidx] = edefs[0] samplerow = self.e_defs[i] for edef_ in edefs[1:]: - row = samplerow[:] - row[colidx] = edef_ + row = [ed.clone() for i, ed in enumerate(samplerow) + if i != colidx] + row.insert(colidx, edef_) self.e_defs.append(row) # now, see if this entity def is referenced as subject in some relation # definition @@ -560,15 +560,16 @@ if isinstance(subj, basestring): subj = typed_eid(subj) elif not isinstance(subj, (int, long)): - subj = subj.eid + subj = subj.entity.eid if isinstance(obj, basestring): obj = typed_eid(obj) elif not isinstance(obj, (int, long)): - obj = obj.eid + obj = obj.entity.eid if repo.schema.rschema(rtype).inlined: entity = session.entity_from_eid(subj) - entity[rtype] = obj - repo.glob_update_entity(session, entity, set((rtype,))) + edited = EditedEntity(entity) + edited.edited_attribute(rtype, obj) + repo.glob_update_entity(session, edited) else: repo.glob_add_relation(session, subj, rtype, obj) diff -r b8287e54b528 -r 8bc6eac1fac1 server/repository.py --- a/server/repository.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/repository.py Wed Aug 25 10:29:18 2010 +0200 @@ -55,6 +55,7 @@ from cubicweb.server import utils, hook, pool, querier, sources from cubicweb.server.session import Session, InternalSession, InternalManager, \ security_enabled +from cubicweb.server.ssplanner import EditedEntity def del_existing_rel_if_needed(session, eidfrom, rtype, eidto): @@ -536,8 +537,7 @@ password = password.encode('UTF8') kwargs['login'] = login kwargs['upassword'] = password - user.update(kwargs) - self.glob_add_entity(session, user) + self.glob_add_entity(session, EditedEntity(user, **kwargs)) session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"', {'x': user.eid}) if email or '@' in login: @@ -940,7 +940,6 @@ self._extid_cache[cachekey] = eid self._type_source_cache[eid] = (etype, source.uri, extid) entity = source.before_entity_insertion(session, extid, etype, eid) - entity.edited_attributes = set(entity.cw_attr_cache) if source.should_call_hooks: self.hm.call_hooks('before_add_entity', session, entity=entity) # XXX call add_info with complete=False ? @@ -1043,15 +1042,16 @@ self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid) return extid - def glob_add_entity(self, session, entity): + def glob_add_entity(self, session, edited): """add an entity to the repository the entity eid should originaly be None and a unique eid is assigned to the entity instance """ - # init edited_attributes before calling before_add_entity hooks + entity = edited.entity entity._cw_is_saved = False # entity has an eid but is not yet saved - entity.edited_attributes = set(entity.cw_attr_cache) # XXX cw_edited_attributes + # init edited_attributes before calling before_add_entity hooks + entity.cw_edited = edited eschema = entity.e_schema source = self.locate_etype_source(entity.__regid__) # allocate an eid to the entity before calling hooks @@ -1063,17 +1063,15 @@ relations = [] if source.should_call_hooks: self.hm.call_hooks('before_add_entity', session, entity=entity) - # XXX use entity.keys here since edited_attributes is not updated for - # inline relations XXX not true, right? (see edited_attributes - # affectation above) - for attr in entity.cw_attr_cache.iterkeys(): + for attr in edited.iterkeys(): rschema = eschema.subjrels[attr] if not rschema.final: # inlined relation - relations.append((attr, entity[attr])) - entity._cw_set_defaults() + relations.append((attr, edited[attr])) + edited.set_defaults() if session.is_hook_category_activated('integrity'): - entity._cw_check(creation=True) + edited.check(creation=True) source.add_entity(session, entity) + edited.saved = True self.add_info(session, entity, source, extid, complete=False) entity._cw_is_saved = True # entity has an eid and is saved # prefill entity relation caches @@ -1082,7 +1080,7 @@ if rtype in schema.VIRTUAL_RTYPES: continue if rschema.final: - entity.setdefault(rtype, None) + entity.cw_attr_cache.setdefault(rtype, None) else: entity.cw_set_relation_cache(rtype, 'subject', session.empty_rset()) @@ -1105,23 +1103,24 @@ eidfrom=entity.eid, rtype=attr, eidto=value) return entity.eid - def glob_update_entity(self, session, entity, edited_attributes): + def glob_update_entity(self, session, edited): """replace an entity in the repository the type and the eid of an entity must not be changed """ + entity = edited.entity if server.DEBUG & server.DBG_REPO: print 'UPDATE entity', entity.__regid__, entity.eid, \ - entity.cw_attr_cache, edited_attributes + entity.cw_attr_cache, edited hm = self.hm eschema = entity.e_schema session.set_entity_cache(entity) - orig_edited_attributes = getattr(entity, 'edited_attributes', None) - entity.edited_attributes = edited_attributes + orig_edited = getattr(entity, 'cw_edited', None) + entity.cw_edited = edited try: only_inline_rels, need_fti_update = True, False relations = [] source = self.source_from_eid(entity.eid, session) - for attr in list(edited_attributes): + for attr in list(edited): if attr == 'eid': continue rschema = eschema.subjrels[attr] @@ -1134,13 +1133,13 @@ previous_value = entity.related(attr) or None if previous_value is not None: previous_value = previous_value[0][0] # got a result set - if previous_value == entity[attr]: + if previous_value == entity.cw_attr_cache[attr]: previous_value = None elif source.should_call_hooks: hm.call_hooks('before_delete_relation', session, eidfrom=entity.eid, rtype=attr, eidto=previous_value) - relations.append((attr, entity[attr], previous_value)) + relations.append((attr, edited[attr], previous_value)) if source.should_call_hooks: # call hooks for inlined relations for attr, value, _ in relations: @@ -1149,8 +1148,9 @@ if not only_inline_rels: hm.call_hooks('before_update_entity', session, entity=entity) if session.is_hook_category_activated('integrity'): - entity._cw_check() + edited.check() source.update_entity(session, entity) + edited.saved = True self.system_source.update_info(session, entity, need_fti_update) if source.should_call_hooks: if not only_inline_rels: @@ -1172,8 +1172,8 @@ hm.call_hooks('after_add_relation', session, eidfrom=entity.eid, rtype=attr, eidto=value) finally: - if orig_edited_attributes is not None: - entity.edited_attributes = orig_edited_attributes + if orig_edited is not None: + entity.cw_edited = orig_edited def glob_delete_entity(self, session, eid): """delete an entity and all related entities from the repository""" diff -r b8287e54b528 -r 8bc6eac1fac1 server/session.py --- a/server/session.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/session.py Wed Aug 25 10:29:18 2010 +0200 @@ -739,51 +739,50 @@ try: # by default, operations are executed with security turned off with security_enabled(self, False, False): - for trstate in ('precommit', 'commit'): - processed = [] - self.commit_state = trstate - try: - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - processed.append(operation) - operation.handle_event('%s_event' % trstate) - self.pending_operations[:] = processed - self.debug('%s session %s done', trstate, self.id) - except: - # if error on [pre]commit: - # - # * set .failed = True on the operation causing the failure - # * call revert_event on processed operations - # * call rollback_event on *all* operations - # - # that seems more natural than not calling rollback_event - # for processed operations, and allow generic rollback - # instead of having to implements rollback, revertprecommit - # and revertcommit, that will be enough in mont case. - operation.failed = True - for operation in reversed(processed): - try: - operation.handle_event('revert%s_event' % trstate) - except: - self.critical('error while reverting %sing', trstate, - exc_info=True) - # XXX use slice notation since self.pending_operations is a - # read-only property. - self.pending_operations[:] = processed + self.pending_operations - self.rollback(reset_pool) - raise + processed = [] + self.commit_state = 'precommit' + try: + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = 'precommit' + processed.append(operation) + operation.handle_event('precommit_event') + self.pending_operations[:] = processed + self.debug('precommit session %s done', self.id) + except: + # if error on [pre]commit: + # + # * set .failed = True on the operation causing the failure + # * call revert_event on processed operations + # * call rollback_event on *all* operations + # + # that seems more natural than not calling rollback_event + # for processed operations, and allow generic rollback + # instead of having to implements rollback, revertprecommit + # and revertcommit, that will be enough in mont case. + operation.failed = True + for operation in reversed(processed): + try: + operation.handle_event('revertprecommit_event') + except: + self.critical('error while reverting precommit', + exc_info=True) + # XXX use slice notation since self.pending_operations is a + # read-only property. + self.pending_operations[:] = processed + self.pending_operations + self.rollback(reset_pool) + raise self.pool.commit() - self.commit_state = trstate = 'postcommit' + self.commit_state = 'postcommit' while self.pending_operations: operation = self.pending_operations.pop(0) - operation.processed = trstate + operation.processed = 'postcommit' try: - operation.handle_event('%s_event' % trstate) + operation.handle_event('postcommit_event') except: - self.critical('error while %sing', trstate, + self.critical('error while postcommit', exc_info=sys.exc_info()) - self.debug('%s session %s done', trstate, self.id) + self.debug('postcommit session %s done', self.id) return self.transaction_uuid(set=False) finally: self._touch() diff -r b8287e54b528 -r 8bc6eac1fac1 server/sources/__init__.py --- a/server/sources/__init__.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/sources/__init__.py Wed Aug 25 10:29:18 2010 +0200 @@ -26,6 +26,7 @@ from cubicweb import set_log_methods, server from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX +from cubicweb.server.ssplanner import EditedEntity def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'): @@ -343,6 +344,7 @@ """ entity = self.repo.vreg['etypes'].etype_class(etype)(session) entity.eid = eid + entity.cw_edited = EditedEntity(entity) return entity def after_entity_insertion(self, session, lid, entity): diff -r b8287e54b528 -r 8bc6eac1fac1 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/sources/ldapuser.py Wed Aug 25 10:29:18 2010 +0200 @@ -574,7 +574,7 @@ entity = super(LDAPUserSource, self).before_entity_insertion(session, lid, etype, eid) res = self._search(session, lid, BASE)[0] for attr in entity.e_schema.indexable_attributes(): - entity[attr] = res[self.user_rev_attrs[attr]] + entity.cw_edited[attr] = res[self.user_rev_attrs[attr]] return entity def after_entity_insertion(self, session, dn, entity): diff -r b8287e54b528 -r 8bc6eac1fac1 server/sources/native.py --- a/server/sources/native.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/sources/native.py Wed Aug 25 10:29:18 2010 +0200 @@ -54,6 +54,7 @@ from cubicweb.server.rqlannotation import set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp from cubicweb.server.session import hooks_control, security_enabled +from cubicweb.server.ssplanner import EditedEntity from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results from cubicweb.server.sources.rql2sql import SQLGenerator @@ -546,21 +547,20 @@ etype = entity.__regid__ for attr, storage in self._storages.get(etype, {}).items(): try: - edited = entity.edited_attributes + edited = entity.cw_edited except AttributeError: assert event == 'deleted' getattr(storage, 'entity_deleted')(entity, attr) else: if attr in edited: handler = getattr(storage, 'entity_%s' % event) - real_value = handler(entity, attr) - restore_values[attr] = real_value + restore_values[attr] = handler(entity, attr) try: yield # 2/ execute the source's instructions finally: # 3/ restore original values for attr, value in restore_values.items(): - entity[attr] = value + entity.cw_edited.edited_attribute(attr, value) def add_entity(self, session, entity): """add a new entity to the source""" @@ -1108,6 +1108,7 @@ err("can't restore entity %s of type %s, type no more supported" % (eid, etype)) return errors + entity.cw_edited = edited = EditedEntity(entity) # check for schema changes, entities linked through inlined relation # still exists, rewrap binary values eschema = entity.e_schema @@ -1124,15 +1125,14 @@ assert value is None elif eschema.destination(rtype) in ('Bytes', 'Password'): action.changes[column] = self._binary(value) - entity[rtype] = Binary(value) + edited[rtype] = Binary(value) elif isinstance(value, str): - entity[rtype] = unicode(value, session.encoding, 'replace') + edited[rtype] = unicode(value, session.encoding, 'replace') else: - entity[rtype] = value + edited[rtype] = value entity.eid = eid session.repo.init_entity_caches(session, entity, self) - entity.edited_attributes = set(entity) - entity._cw_check() + edited.check() self.repo.hm.call_hooks('before_add_entity', session, entity=entity) # restore the entity action.changes['cw_eid'] = eid diff -r b8287e54b528 -r 8bc6eac1fac1 server/sources/storages.py --- a/server/sources/storages.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/sources/storages.py Wed Aug 25 10:29:18 2010 +0200 @@ -23,6 +23,8 @@ from cubicweb import Binary, ValidationError from cubicweb.server import hook +from cubicweb.server.ssplanner import EditedEntity + def set_attribute_storage(repo, etype, attr, storage): repo.system_source.set_storage(etype, attr, storage) @@ -30,6 +32,7 @@ def unset_attribute_storage(repo, etype, attr): repo.system_source.unset_storage(etype, attr) + class Storage(object): """abstract storage @@ -114,12 +117,12 @@ def entity_added(self, entity, attr): """an entity using this storage for attr has been added""" if entity._cw.transaction_data.get('fs_importing'): - binary = Binary(file(entity[attr].getvalue(), 'rb').read()) + binary = Binary(file(entity.cw_edited[attr].getvalue(), 'rb').read()) else: - binary = entity.pop(attr) + binary = entity.cw_edited.pop(attr) fpath = self.new_fs_path(entity, attr) # bytes storage used to store file's path - entity[attr] = Binary(fpath) + entity.cw_edited.edited_attribute(attr, Binary(fpath)) file(fpath, 'wb').write(binary.getvalue()) hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp) return binary @@ -132,7 +135,7 @@ # If we are importing from the filesystem, the file already exists. # We do not need to create it but we need to fetch the content of # the file as the actual content of the attribute - fpath = entity[attr].getvalue() + fpath = entity.cw_edited[attr].getvalue() binary = Binary(file(fpath, 'rb').read()) else: # We must store the content of the attributes @@ -144,7 +147,7 @@ # went ok. # # fetch the current attribute value in memory - binary = entity.pop(attr) + binary = entity.cw_edited.pop(attr) # Get filename for it fpath = self.new_fs_path(entity, attr) assert not osp.exists(fpath) @@ -155,7 +158,7 @@ hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp) if oldpath != fpath: # register the new location for the file. - entity[attr] = Binary(fpath) + entity.cw_edited.edited_attribute(attr, Binary(fpath)) # Mark the old file as useless so the file will be removed at # commit. hook.set_operation(entity._cw, 'bfss_deleted', oldpath, @@ -197,7 +200,7 @@ def migrate_entity(self, entity, attribute): """migrate an entity attribute to the storage""" - entity.edited_attributes = set() + entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache) self.entity_added(entity, attribute) session = entity._cw source = session.repo.system_source @@ -205,6 +208,7 @@ sql = source.sqlgen.update('cw_' + entity.__regid__, attrs, ['cw_eid']) source.doexec(session, sql, attrs) + entity.cw_edited = None class AddFileOp(hook.Operation): @@ -216,7 +220,7 @@ self.error('cant remove %s: %s' % (filepath, ex)) class DeleteFileOp(hook.Operation): - def commit_event(self): + def postcommit_event(self): for filepath in self.session.transaction_data.pop('bfss_deleted'): try: unlink(filepath) diff -r b8287e54b528 -r 8bc6eac1fac1 server/sqlutils.py --- a/server/sqlutils.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/sqlutils.py Wed Aug 25 10:29:18 2010 +0200 @@ -260,11 +260,10 @@ """ attrs = {} eschema = entity.e_schema - for attr in entity.edited_attributes: - value = entity[attr] + for attr, value in entity.cw_edited.iteritems(): rschema = eschema.subjrels[attr] if rschema.final: - atype = str(entity.e_schema.destination(attr)) + atype = str(eschema.destination(attr)) if atype == 'Boolean': value = self.dbhelper.boolean_value(value) elif atype == 'Password': diff -r b8287e54b528 -r 8bc6eac1fac1 server/ssplanner.py --- a/server/ssplanner.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/ssplanner.py Wed Aug 25 10:29:18 2010 +0200 @@ -21,6 +21,8 @@ __docformat__ = "restructuredtext en" +from copy import copy + from rql.stmts import Union, Select from rql.nodes import Constant, Relation @@ -55,11 +57,11 @@ if isinstance(rhs, Constant) and not rhs.uid: # add constant values to entity def value = rhs.eval(plan.args) - eschema = edef.e_schema + eschema = edef.entity.e_schema attrtype = eschema.subjrels[rtype].objects(eschema)[0] if attrtype == 'Password' and isinstance(value, unicode): value = value.encode('UTF8') - edef[rtype] = value + edef.edited_attribute(rtype, value) elif to_build.has_key(str(rhs)): # create a relation between two newly created variables plan.add_relation_def((edef, rtype, to_build[rhs.name])) @@ -126,6 +128,132 @@ return select +_MARKER = object() + +class dict_protocol_catcher(object): + def __init__(self, entity): + self.__entity = entity + def __getitem__(self, attr): + return self.__entity.cw_edited[attr] + def __setitem__(self, attr, value): + self.__entity.cw_edited[attr] = value + def __getattr__(self, attr): + return getattr(self.__entity, attr) + + +class EditedEntity(dict): + """encapsulate entities attributes being written by an RQL query""" + def __init__(self, entity, **kwargs): + dict.__init__(self, **kwargs) + self.entity = entity + self.skip_security = set() + self.querier_pending_relations = {} + self.saved = False + + def __hash__(self): + # dict|set keyable + return hash(id(self)) + + def __cmp__(self, other): + # we don't want comparison by value inherited from dict + return cmp(id(self), id(other)) + + def __setitem__(self, attr, value): + assert attr != 'eid' + # don't add attribute into skip_security if already in edited + # attributes, else we may accidentaly skip a desired security check + if attr not in self: + self.skip_security.add(attr) + self.edited_attribute(attr, value) + + def __delitem__(self, attr): + assert not self.saved, 'too late to modify edited attributes' + super(EditedEntity, self).__delitem__(attr) + self.entity.cw_attr_cache.pop(attr, None) + + def pop(self, attr, *args): + # don't update skip_security by design (think to storage api) + assert not self.saved, 'too late to modify edited attributes' + value = super(EditedEntity, self).pop(attr, *args) + self.entity.cw_attr_cache.pop(attr, *args) + return value + + def setdefault(self, attr, default): + assert attr != 'eid' + # don't add attribute into skip_security if already in edited + # attributes, else we may accidentaly skip a desired security check + if attr not in self: + self[attr] = default + return self[attr] + + def update(self, values, skipsec=True): + if skipsec: + setitem = self.__setitem__ + else: + setitem = self.edited_attribute + for attr, value in values.iteritems(): + setitem(attr, value) + + def edited_attribute(self, attr, value): + """attribute being edited by a rql query: should'nt be added to + skip_security + """ + assert not self.saved, 'too late to modify edited attributes' + super(EditedEntity, self).__setitem__(attr, value) + self.entity.cw_attr_cache[attr] = value + + def oldnewvalue(self, attr): + """returns the couple (old attr value, new attr value) + + NOTE: will only work in a before_update_entity hook + """ + assert not self.saved, 'too late to get the old value' + # get new value and remove from local dict to force a db query to + # fetch old value + newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER) + oldvalue = getattr(self.entity, attr) + if newvalue is not _MARKER: + self.entity.cw_attr_cache[attr] = newvalue + else: + newvalue = oldvalue + return oldvalue, newvalue + + def set_defaults(self): + """set default values according to the schema""" + for attr, value in self.entity.e_schema.defaults(): + if not attr in self: + self[str(attr)] = value + + def check(self, creation=False): + """check the entity edition against its schema. Only final relation + are checked here, constraint on actual relations are checked in hooks + """ + entity = self.entity + if creation: + # on creations, we want to check all relations, especially + # required attributes + relations = [rschema for rschema in entity.e_schema.subject_relations() + if rschema.final and rschema.type != 'eid'] + else: + relations = [entity._cw.vreg.schema.rschema(rtype) + for rtype in self] + from yams import ValidationError + try: + entity.e_schema.check(dict_protocol_catcher(entity), + creation=creation, _=entity._cw._, + relations=relations) + except ValidationError, ex: + ex.entity = self.entity + raise + + def clone(self): + thecopy = EditedEntity(copy(self.entity)) + thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache) + thecopy.entity._cw_related_cache = {} + thecopy.update(self, skipsec=False) + return thecopy + + class SSPlanner(object): """SingleSourcePlanner: build execution plan for rql queries @@ -162,7 +290,7 @@ etype_class = session.vreg['etypes'].etype_class for etype, var in rqlst.main_variables: # need to do this since entity class is shared w. web client code ! - to_build[var.name] = etype_class(etype)(session) + to_build[var.name] = EditedEntity(etype_class(etype)(session)) plan.add_entity_def(to_build[var.name]) # add constant values to entity def, mark variables to be selected to_select = _extract_const_attributes(plan, rqlst, to_build) @@ -177,7 +305,7 @@ for edef, rdefs in to_select.items(): # create a select rql st to fetch needed data select = Select() - eschema = edef.e_schema + eschema = edef.entity.e_schema for i, (rtype, term, reverse) in enumerate(rdefs): if getattr(term, 'variable', None) in eidconsts: value = eidconsts[term.variable] @@ -284,10 +412,8 @@ rhsinfo = selectedidx[rhskey][:-1] + (None,) rschema = getrschema(relation.r_type) updatedefs.append( (lhsinfo, rhsinfo, rschema) ) - if rschema.final or rschema.inlined: - attributes.add(relation.r_type) # the update step - step = UpdateStep(plan, updatedefs, attributes) + step = UpdateStep(plan, updatedefs) # when necessary add substep to fetch yet unknown values select = _build_substep_query(select, rqlst) if select is not None: @@ -476,7 +602,7 @@ result = [[]] for row in result: # get a new entity definition for this row - edef = base_edef.cw_copy() + edef = base_edef.clone() # complete this entity def using row values index = 0 for rtype, rorder, value in self.rdefs: @@ -484,7 +610,7 @@ value = row[index] index += 1 if rorder == InsertRelationsStep.FINAL: - edef._cw_rql_set_value(rtype, value) + edef.edited_attribute(rtype, value) elif rorder == InsertRelationsStep.RELATION: self.plan.add_relation_def( (edef, rtype, value) ) edef.querier_pending_relations[(rtype, 'subject')] = value @@ -495,6 +621,7 @@ self.plan.substitute_entity_def(base_edef, edefs) return result + class InsertStep(Step): """step consisting in inserting new entities / relations""" @@ -555,10 +682,9 @@ definitions and from results fetched in previous step """ - def __init__(self, plan, updatedefs, attributes): + def __init__(self, plan, updatedefs): Step.__init__(self, plan) self.updatedefs = updatedefs - self.attributes = attributes def execute(self): """execute this step""" @@ -578,16 +704,17 @@ if rschema.final or rschema.inlined: eid = typed_eid(lhsval) try: - edef = edefs[eid] + edited = edefs[eid] except KeyError: - edefs[eid] = edef = session.entity_from_eid(eid) - edef._cw_rql_set_value(str(rschema), rhsval) + edef = session.entity_from_eid(eid) + edefs[eid] = edited = EditedEntity(edef) + edited.edited_attribute(str(rschema), rhsval) else: repo.glob_add_relation(session, lhsval, str(rschema), rhsval) result[i] = newrow # update entities - for eid, edef in edefs.iteritems(): - repo.glob_update_entity(session, edef, set(self.attributes)) + for eid, edited in edefs.iteritems(): + repo.glob_update_entity(session, edited) return result def _handle_relterm(info, row, newrow): diff -r b8287e54b528 -r 8bc6eac1fac1 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Wed Aug 25 10:29:07 2010 +0200 +++ b/server/test/unittest_repository.py Wed Aug 25 10:29:18 2010 +0200 @@ -423,7 +423,7 @@ 'EmailAddress', address=u'a@b.fr') def test_multiple_edit_set_attributes(self): - """make sure edited_attributes doesn't get cluttered + """make sure cw_edited doesn't get cluttered by previous entities on multiple set """ # local hook @@ -434,9 +434,9 @@ events = ('before_update_entity',) def __call__(self): # invoiced attribute shouldn't be considered "edited" before the hook - self._test.failIf('invoiced' in self.entity.edited_attributes, - 'edited_attributes cluttered by previous update') - self.entity['invoiced'] = 10 + self._test.failIf('invoiced' in self.entity.cw_edited, + 'cw_edited cluttered by previous update') + self.entity.cw_edited['invoiced'] = 10 with self.temporary_appobjects(DummyBeforeHook): req = self.request() req.create_entity('Affaire', ref=u'AFF01') diff -r b8287e54b528 -r 8bc6eac1fac1 sobjects/supervising.py --- a/sobjects/supervising.py Wed Aug 25 10:29:07 2010 +0200 +++ b/sobjects/supervising.py Wed Aug 25 10:29:18 2010 +0200 @@ -15,10 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""some hooks and views to handle supervising of any data changes +"""some hooks and views to handle supervising of any data changes""" - -""" __docformat__ = "restructuredtext en" from cubicweb import UnknownEid @@ -185,6 +183,6 @@ msg = format_mail(uinfo, recipients, content, view.subject(), config=config) self.to_send = [(msg, recipients)] - def commit_event(self): + def postcommit_event(self): self._prepare_email() - SendMailOp.commit_event(self) + SendMailOp.postcommit_event(self) diff -r b8287e54b528 -r 8bc6eac1fac1 test/unittest_entity.py --- a/test/unittest_entity.py Wed Aug 25 10:29:07 2010 +0200 +++ b/test/unittest_entity.py Wed Aug 25 10:29:18 2010 +0200 @@ -322,30 +322,30 @@ content_format=u'text/rest') self.assertEquals(e.printable_value('content'), '

du *ReST*

\n') - e['content'] = 'du html users' - e['content_format'] = 'text/html' + e.cw_attr_cache['content'] = 'du html users' + e.cw_attr_cache['content_format'] = 'text/html' self.assertEquals(e.printable_value('content'), 'du html users') - e['content'] = 'du *texte*' - e['content_format'] = 'text/plain' + e.cw_attr_cache['content'] = 'du *texte*' + e.cw_attr_cache['content_format'] = 'text/plain' self.assertEquals(e.printable_value('content'), '

\ndu *texte*\n

') - e['title'] = 'zou' - e['content'] = '''\ + e.cw_attr_cache['title'] = 'zou' + e.cw_attr_cache['content'] = '''\ a title ======= du :eid:`1:*ReST*`''' - e['content_format'] = 'text/rest' + e.cw_attr_cache['content_format'] = 'text/rest' self.assertEquals(e.printable_value('content', format='text/plain'), - e['content']) + e.cw_attr_cache['content']) - e['content'] = u'yo (zou éà ;)' - e['content_format'] = 'text/html' + e.cw_attr_cache['content'] = u'yo (zou éà ;)' + e.cw_attr_cache['content_format'] = 'text/html' self.assertEquals(e.printable_value('content', format='text/plain').strip(), u'**yo (zou éà ;)**') if HAS_TAL: - e['content'] = '

titre

' - e['content_format'] = 'text/cubicweb-page-template' + e.cw_attr_cache['content'] = '

titre

' + e.cw_attr_cache['content_format'] = 'text/cubicweb-page-template' self.assertEquals(e.printable_value('content'), '

zou

') @@ -387,30 +387,30 @@ tidy = lambda x: x.replace('\n', '') self.assertEquals(tidy(e.printable_value('content')), '
R&D
') - e['content'] = u'yo !! R&D
pas fermé' + e.cw_attr_cache['content'] = u'yo !! R&D
pas fermé' self.assertEquals(tidy(e.printable_value('content')), u'yo !! R&D
pas fermé
') - e['content'] = u'R&D' + e.cw_attr_cache['content'] = u'R&D' self.assertEquals(tidy(e.printable_value('content')), u'R&D') - e['content'] = u'R&D;' + e.cw_attr_cache['content'] = u'R&D;' self.assertEquals(tidy(e.printable_value('content')), u'R&D;') - e['content'] = u'yo !! R&D
pas fermé' + e.cw_attr_cache['content'] = u'yo !! R&D
pas fermé' self.assertEquals(tidy(e.printable_value('content')), u'yo !! R&D
pas fermé
') - e['content'] = u'été
été' + e.cw_attr_cache['content'] = u'été
été' self.assertEquals(tidy(e.printable_value('content')), u'été
été
') - e['content'] = u'C'est un exemple sérieux' + e.cw_attr_cache['content'] = u'C'est un exemple sérieux' self.assertEquals(tidy(e.printable_value('content')), u"C'est un exemple sérieux") # make sure valid xhtml is left untouched - e['content'] = u'
R&D
' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'
été
' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'été' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'hop\r\nhop\nhip\rmomo' + e.cw_attr_cache['content'] = u'
R&D
' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'
été
' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'été' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'hop\r\nhop\nhip\rmomo' self.assertEquals(e.printable_value('content'), u'hop\nhop\nhip\nmomo') def test_printable_value_bad_html_ms(self): @@ -419,7 +419,7 @@ e = req.create_entity('Card', title=u'bad html', content=u'
R&D
', content_format=u'text/html') tidy = lambda x: x.replace('\n', '') - e['content'] = u'
ms orifice produces weird html
' + e.cw_attr_cache['content'] = u'
ms orifice produces weird html
' self.assertEquals(tidy(e.printable_value('content')), u'
ms orifice produces weird html
') import tidy as tidymod # apt-get install python-tidy @@ -435,12 +435,12 @@ def test_fulltextindex(self): e = self.vreg['etypes'].etype_class('File')(self.request()) - e['description'] = 'du html' - e['description_format'] = 'text/html' - e['data'] = Binary('some data') - e['data_name'] = 'an html file' - e['data_format'] = 'text/html' - e['data_encoding'] = 'ascii' + e.cw_attr_cache['description'] = 'du html' + e.cw_attr_cache['description_format'] = 'text/html' + e.cw_attr_cache['data'] = Binary('some data') + e.cw_attr_cache['data_name'] = 'an html file' + e.cw_attr_cache['data_format'] = 'text/html' + e.cw_attr_cache['data_encoding'] = 'ascii' e._cw.transaction_data = {} # XXX req should be a session self.assertEquals(e.cw_adapt_to('IFTIndexable').get_words(), {'C': [u'du', u'html', 'an', 'html', 'file', u'some', u'data']}) @@ -461,7 +461,7 @@ 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0) trinfo.complete() - self.failUnless(isinstance(trinfo['creation_date'], datetime)) + self.failUnless(isinstance(trinfo.cw_attr_cache['creation_date'], datetime)) self.failUnless(trinfo.cw_relation_cached('from_state', 'subject')) self.failUnless(trinfo.cw_relation_cached('to_state', 'subject')) self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject')) diff -r b8287e54b528 -r 8bc6eac1fac1 test/unittest_rset.py --- a/test/unittest_rset.py Wed Aug 25 10:29:07 2010 +0200 +++ b/test/unittest_rset.py Wed Aug 25 10:29:18 2010 +0200 @@ -157,13 +157,13 @@ rs.req = self.request() rs.vreg = self.vreg - rs2 = rs.sorted_rset(lambda e:e['login']) + rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login']) self.assertEquals(len(rs2), 3) self.assertEquals([login for _, login in rs2], ['adim', 'nico', 'syt']) # make sure rs is unchanged self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico']) - rs2 = rs.sorted_rset(lambda e:e['login'], reverse=True) + rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'], reverse=True) self.assertEquals(len(rs2), 3) self.assertEquals([login for _, login in rs2], ['syt', 'nico', 'adim']) # make sure rs is unchanged @@ -187,7 +187,7 @@ rs.req = self.request() rs.vreg = self.vreg - rsets = rs.split_rset(lambda e:e['login']) + rsets = rs.split_rset(lambda e:e.cw_attr_cache['login']) self.assertEquals(len(rsets), 3) self.assertEquals([login for _, login,_ in rsets[0]], ['adim', 'adim']) self.assertEquals([login for _, login,_ in rsets[1]], ['syt']) @@ -195,7 +195,7 @@ # make sure rs is unchanged self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico']) - rsets = rs.split_rset(lambda e:e['login'], return_dict=True) + rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'], return_dict=True) self.assertEquals(len(rsets), 3) self.assertEquals([login for _, login,_ in rsets['nico']], ['nico', 'nico']) self.assertEquals([login for _, login,_ in rsets['adim']], ['adim', 'adim']) @@ -230,12 +230,12 @@ self.request().create_entity('CWUser', login=u'adim', upassword='adim', surname=u'di mascio', firstname=u'adrien') e = self.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0) - self.assertEquals(e['surname'], 'di mascio') - self.assertRaises(KeyError, e.__getitem__, 'firstname') - self.assertRaises(KeyError, e.__getitem__, 'creation_date') + self.assertEquals(e.cw_attr_cache['surname'], 'di mascio') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'creation_date') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e.complete() - self.assertEquals(e['firstname'], 'adrien') + self.assertEquals(e.cw_attr_cache['firstname'], 'adrien') self.assertEquals(pprelcachedict(e._cw_related_cache), []) def test_get_entity_advanced(self): @@ -246,20 +246,20 @@ e = rset.get_entity(0, 0) self.assertEquals(e.cw_row, 0) self.assertEquals(e.cw_col, 0) - self.assertEquals(e['title'], 'zou') - self.assertRaises(KeyError, e.__getitem__, 'path') + self.assertEquals(e.cw_attr_cache['title'], 'zou') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'path') self.assertEquals(e.view('text'), 'zou') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e = rset.get_entity(0, 1) self.assertEquals(e.cw_row, 0) self.assertEquals(e.cw_col, 1) - self.assertEquals(e['login'], 'anon') - self.assertRaises(KeyError, e.__getitem__, 'firstname') + self.assertEquals(e.cw_attr_cache['login'], 'anon') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e.complete() - self.assertEquals(e['firstname'], None) + self.assertEquals(e.cw_attr_cache['firstname'], None) self.assertEquals(e.view('text'), 'anon') self.assertEquals(pprelcachedict(e._cw_related_cache), []) @@ -282,17 +282,17 @@ rset = self.execute('Any X,U,S,XT,UL,SN WHERE X created_by U, U in_state S, ' 'X title XT, S name SN, U login UL, X eid %s' % e.eid) e = rset.get_entity(0, 0) - self.assertEquals(e['title'], 'zou') + self.assertEquals(e.cw_attr_cache['title'], 'zou') self.assertEquals(pprelcachedict(e._cw_related_cache), [('created_by_subject', [5])]) # first level of recursion u = e.created_by[0] - self.assertEquals(u['login'], 'admin') - self.assertRaises(KeyError, u.__getitem__, 'firstname') + self.assertEquals(u.cw_attr_cache['login'], 'admin') + self.assertRaises(KeyError, u.cw_attr_cache.__getitem__, 'firstname') # second level of recursion s = u.in_state[0] - self.assertEquals(s['name'], 'activated') - self.assertRaises(KeyError, s.__getitem__, 'description') + self.assertEquals(s.cw_attr_cache['name'], 'activated') + self.assertRaises(KeyError, s.cw_attr_cache.__getitem__, 'description') def test_get_entity_cache_with_left_outer_join(self): @@ -322,7 +322,7 @@ etype, n = expected[entity.cw_row] self.assertEquals(entity.__regid__, etype) attr = etype == 'Bookmark' and 'title' or 'name' - self.assertEquals(entity[attr], n) + self.assertEquals(entity.cw_attr_cache[attr], n) def test_related_entity_optional(self): e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path') diff -r b8287e54b528 -r 8bc6eac1fac1 test/unittest_utils.py --- a/test/unittest_utils.py Wed Aug 25 10:29:07 2010 +0200 +++ b/test/unittest_utils.py Wed Aug 25 10:29:18 2010 +0200 @@ -140,14 +140,14 @@ def test_encoding_bare_entity(self): e = Entity(None) - e['pouet'] = 'hop' + e.cw_attr_cache['pouet'] = 'hop' e.eid = 2 self.assertEquals(json.loads(self.encode(e)), {'pouet': 'hop', 'eid': 2}) def test_encoding_entity_in_list(self): e = Entity(None) - e['pouet'] = 'hop' + e.cw_attr_cache['pouet'] = 'hop' e.eid = 2 self.assertEquals(json.loads(self.encode([e])), [{'pouet': 'hop', 'eid': 2}]) diff -r b8287e54b528 -r 8bc6eac1fac1 web/formfields.py --- a/web/formfields.py Wed Aug 25 10:29:07 2010 +0200 +++ b/web/formfields.py Wed Aug 25 10:29:18 2010 +0200 @@ -333,7 +333,7 @@ if self.eidparam and self.role is not None: entity = form.edited_entity if form._cw.vreg.schema.rschema(self.name).final: - if entity.has_eid() or self.name in entity: + if entity.has_eid() or self.name in entity.cw_attr_cache: value = getattr(entity, self.name) if value is not None or not self.fallback_on_none_attribute: return value @@ -428,7 +428,7 @@ if self.eidparam and self.role == 'subject': entity = form.edited_entity if entity.e_schema.has_metadata(self.name, 'format') and ( - entity.has_eid() or '%s_format' % self.name in entity): + entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache): return form.edited_entity.cw_attr_metadata(self.name, 'format') return form._cw.property_value('ui.default-text-format') diff -r b8287e54b528 -r 8bc6eac1fac1 web/views/cwproperties.py --- a/web/views/cwproperties.py Wed Aug 25 10:29:07 2010 +0200 +++ b/web/views/cwproperties.py Wed Aug 25 10:29:18 2010 +0200 @@ -200,8 +200,8 @@ else: entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw) entity.eid = self._cw.varmaker.next() - entity['pkey'] = key - entity['value'] = self._cw.vreg.property_value(key) + entity.cw_attr_cache['pkey'] = key + entity.cw_attr_cache['value'] = self._cw.vreg.property_value(key) return entity def form(self, formid, keys, splitlabel=False): @@ -329,7 +329,7 @@ def form_init(self, form): entity = form.edited_entity - if not (entity.has_eid() or 'pkey' in entity): + if not (entity.has_eid() or 'pkey' in entity.cw_attr_cache): # no key set yet, just include an empty div which will be filled # on key selection return diff -r b8287e54b528 -r 8bc6eac1fac1 web/views/xmlrss.py --- a/web/views/xmlrss.py Wed Aug 25 10:29:07 2010 +0200 +++ b/web/views/xmlrss.py Wed Aug 25 10:29:18 2010 +0200 @@ -68,7 +68,7 @@ value = entity.eid else: try: - value = entity[attr] + value = entity.cw_attr_cache[attr] except KeyError: # Bytes continue