Commit 5d3b289d authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[entity api] unify set_attributes / set_relations into a cw_set method. Closes #2423719

Allowing similar usage as create_entity/cw_instantiate.

Update cw code base to remove deprecated calls.
parent 82272decfa99
......@@ -364,7 +364,7 @@ You can prefer use a migration script similar to this shell invocation instead::
>>> crypted = crypt_password('joepass')
>>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
>>> joe = rset.get_entity(0,0)
>>> joe.set_attributes(upassword=Binary(crypted))
>>> joe.cw_set(upassword=Binary(crypted))
Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
......
......@@ -38,7 +38,7 @@ life span, limited to the hook, operation or view within which the
object was built.
Setting an attribute or relation value can be done in the context of a
Hook/Operation, using the obj.set_relations(x=42) notation or a plain
Hook/Operation, using the obj.cw_set(x=42) notation or a plain
RQL SET expression.
In views, it would be preferable to encapsulate the necessary logic in
......
......@@ -47,16 +47,13 @@ classes are registered in order to initialize the class according to its schema:
related to the current entity by the relation given in parameter
and satisfying its constraints
* `set_attributes(**kwargs)`, updates the attributes list with the corresponding
values given named parameters
* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
corresponding values given named parameters. To set a relation where this
entity is the object of the relation, use `reverse_<relation>` as argument
name. Values may be an entity, a list of entities, or None (meaning that all
relations of the given type from or to this object should be deleted).
* `set_relations(**kwargs)`, add relations to the given object. To
set a relation where this entity is the object of the relation,
use `reverse_<relation>` as argument name. Values may be an
entity, a list of entities, or None (meaning that all relations of
the given type from or to this object should be deleted).
* `copy_relations(ceid)`, copies the relations of the entities having the eid
* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
given in the parameters on the current entity
* `delete()` allows to delete the entity
......
......@@ -206,10 +206,11 @@ Hooks writing tips
Reminder
~~~~~~~~
You should never use the `entity.foo = 42` notation to update an
entity. It will not do what you expect (updating the
database). Instead, use the :meth:`set_attributes` and
:meth:`set_relations` methods.
You should never use the `entity.foo = 42` notation to update an entity. It will
not do what you expect (updating the database). Instead, use the
:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
'before_update_entity' event.
How to choose between a before and an after event ?
......
......@@ -70,13 +70,13 @@ from http://www.cubicweb.org/project/cubicweb-keyword).
def test_cannot_create_cycles(self):
# direct obvious cycle
self.assertRaises(ValidationError, self.kw1.set_relations,
self.assertRaises(ValidationError, self.kw1.cw_set,
subkeyword_of=self.kw1)
# testing indirect cycles
kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
'SK subkeyword_of K WHERE C name "classif1", K eid %s'
% self.kw1.eid).get_entity(0,0)
self.kw1.set_relations(subkeyword_of=kw3)
self.kw1.cw_set(subkeyword_of=kw3)
self.assertRaises(ValidationError, self.commit)
The test class defines a :meth:`setup_database` method which populates the
......@@ -192,10 +192,10 @@ Let us look at simple example from the ``blog`` cube.
description=u'cubicweb is beautiful')
blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
content=u'cubicweb hop')
blog_entry_1.set_relations(entry_of=cubicweb_blog)
blog_entry_1.cw_set(entry_of=cubicweb_blog)
blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
content=u'cubicweb yes')
blog_entry_2.set_relations(entry_of=cubicweb_blog)
blog_entry_2.cw_set(entry_of=cubicweb_blog)
self.assertEqual(len(MAILBOX), 0)
self.commit()
self.assertEqual(len(MAILBOX), 2)
......
......@@ -196,7 +196,7 @@ Here is the code in cube's *hooks.py*:
for eid in self.get_data():
entity = self.session.entity_from_eid(eid)
if entity.visibility == 'parent':
entity.set_attributes(visibility=u'authenticated')
entity.cw_set(visibility=u'authenticated')
class SetVisibilityHook(hook.Hook):
__regid__ = 'sytweb.setvisibility'
......@@ -215,7 +215,7 @@ Here is the code in cube's *hooks.py*:
parent = self._cw.entity_from_eid(self.eidto)
child = self._cw.entity_from_eid(self.eidfrom)
if child.visibility == 'parent':
child.set_attributes(visibility=parent.visibility)
child.cw_set(visibility=parent.visibility)
Notice:
......@@ -344,7 +344,7 @@ model, in *test/unittest_sytweb.py*:
self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
# may_be_read_by propagation
self.restore_connection()
folder.set_relations(may_be_read_by=toto)
folder.cw_set(may_be_read_by=toto)
self.commit()
photo1.clear_all_caches()
self.failUnless(photo1.may_be_read_by)
......
......@@ -101,7 +101,7 @@ class CWUser(AnyEntity):
kwargs['for_user'] = self
self._cw.create_entity('CWProperty', **kwargs)
else:
prop.set_attributes(value=value)
prop.cw_set(value=value)
def matching_groups(self, groups):
"""return the number of the given group(s) in which the user is
......
......@@ -51,7 +51,7 @@ class _CWSourceCfgMixIn(object):
continue
raise
cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding)
self.set_attributes(config=cfgstr)
self.cw_set(config=cfgstr)
class CWSource(_CWSourceCfgMixIn, AnyEntity):
......@@ -181,5 +181,5 @@ class CWDataImport(AnyEntity):
def write_log(self, session, **kwargs):
if 'status' not in kwargs:
kwargs['status'] = getattr(self, '_status', u'success')
self.set_attributes(log=u'<br/>'.join(self._logs), **kwargs)
self.cw_set(log=u'<br/>'.join(self._logs), **kwargs)
self._logs = []
......@@ -70,7 +70,7 @@ class EmailAddressTC(BaseEntityTC):
email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
email1.set_relations(prefered_form=email2)
email1.cw_set(prefered_form=email2)
self.assertEqual(email1.prefered.eid, email2.eid)
self.assertEqual(email2.prefered.eid, email2.eid)
self.assertEqual(email3.prefered.eid, email3.eid)
......@@ -104,10 +104,10 @@ class CWUserTC(BaseEntityTC):
e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), 'member')
e.set_attributes(firstname=u'bouah')
e.cw_set(firstname=u'bouah')
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), u'bouah')
e.set_attributes(surname=u'lôt')
e.cw_set(surname=u'lôt')
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), u'bouah lôt')
......
......@@ -63,7 +63,7 @@ class WorkflowBuildingTC(CubicWebTC):
# gnark gnark
bar = wf.add_state(u'bar')
self.commit()
bar.set_attributes(name=u'foo')
bar.cw_set(name=u'foo')
with self.assertRaises(ValidationError) as cm:
self.commit()
self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
......@@ -86,7 +86,7 @@ class WorkflowBuildingTC(CubicWebTC):
# gnark gnark
biz = wf.add_transition(u'biz', (bar,), foo)
self.commit()
biz.set_attributes(name=u'baz')
biz.cw_set(name=u'baz')
with self.assertRaises(ValidationError) as cm:
self.commit()
self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
......@@ -516,7 +516,7 @@ class AutoTransitionTC(CubicWebTC):
['rest'])
self.assertEqual(parse_hist(iworkflowable.workflow_history),
[('asleep', 'asleep', 'rest', None)])
user.set_attributes(surname=u'toto') # fulfill condition
user.cw_set(surname=u'toto') # fulfill condition
self.commit()
iworkflowable.fire_transition('rest')
self.commit()
......
......@@ -452,26 +452,13 @@ class Entity(AppObject):
return mainattr, needcheck
@classmethod
def cw_instantiate(cls, execute, **kwargs):
"""add a new entity of this given type
Example (in a shell session):
>>> companycls = vreg['etypes'].etype_class(('Company')
>>> personcls = vreg['etypes'].etype_class(('Person')
>>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
>>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
... works_for=c)
You can also set relation where the entity has 'object' role by
prefixing the relation by 'reverse_'.
"""
rql = 'INSERT %s X' % cls.__regid__
def _cw_build_entity_query(cls, kwargs):
relations = []
restrictions = set()
pending_relations = []
eschema = cls.e_schema
qargs = {}
attrcache = {}
for attr, value in kwargs.items():
if attr.startswith('reverse_'):
attr = attr[len('reverse_'):]
......@@ -491,6 +478,9 @@ class Entity(AppObject):
continue
if rschema.final: # attribute
relations.append('X %s %%(%s)s' % (attr, attr))
attrcache[attr] = value
elif value is None:
pending_relations.append( (attr, role, value) )
else:
rvar = attr.upper()
if role == 'object':
......@@ -503,19 +493,51 @@ class Entity(AppObject):
if hasattr(value, 'eid'):
value = value.eid
qargs[attr] = value
rql = u''
if relations:
rql = '%s: %s' % (rql, ', '.join(relations))
rql += ', '.join(relations)
if restrictions:
rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
created = execute(rql, qargs).get_entity(0, 0)
rql += ' WHERE %s' % ', '.join(restrictions)
return rql, qargs, pending_relations, attrcache
@classmethod
def _cw_handle_pending_relations(cls, eid, pending_relations, execute):
for attr, role, values in pending_relations:
if role == 'object':
restr = 'Y %s X' % attr
else:
restr = 'X %s Y' % attr
if values is None:
execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
continue
execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
{'x': created.eid}, build_descr=False)
{'x': eid}, build_descr=False)
@classmethod
def cw_instantiate(cls, execute, **kwargs):
"""add a new entity of this given type
Example (in a shell session):
>>> companycls = vreg['etypes'].etype_class(('Company')
>>> personcls = vreg['etypes'].etype_class(('Person')
>>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
>>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
... works_for=c)
You can also set relations where the entity has 'object' role by
prefixing the relation name by 'reverse_'. Also, relation values may be
an entity or eid, a list of entities or eids.
"""
rql, qargs, pending_relations, attrcache = cls._cw_build_entity_query(kwargs)
if rql:
rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
else:
rql = 'INSERT %s X' % (cls.__regid__)
created = execute(rql, qargs).get_entity(0, 0)
created.cw_attr_cache.update(attrcache)
cls._cw_handle_pending_relations(created.eid, pending_relations, execute)
return created
def __init__(self, req, rset=None, row=None, col=0):
......@@ -1212,54 +1234,41 @@ class Entity(AppObject):
# raw edition utilities ###################################################
def set_attributes(self, **kwargs): # XXX cw_set_attributes
def cw_set(self, **kwargs):
"""update this entity using given attributes / relation, working in the
same fashion as :meth:`cw_instantiate`.
Example (in a shell session):
>>> c = rql('Any X WHERE X is Company').get_entity(0, 0)
>>> p = rql('Any X WHERE X is Person').get_entity(0, 0)
>>> c.set(name=u'Logilab')
>>> p.set(firstname=u'John', lastname=u'Doe', works_for=c)
You can also set relations where the entity has 'object' role by
prefixing the relation name by 'reverse_'. Also, relation values may be
an entity or eid, a list of entities or eids, or None (meaning that all
relations of the given type from or to this object should be deleted).
"""
_check_cw_unsafe(kwargs)
assert kwargs
assert self.cw_is_saved(), "should not call set_attributes while entity "\
"hasn't been saved yet"
relations = ['X %s %%(%s)s' % (key, key) for key in kwargs]
# and now update the database
kwargs['x'] = self.eid
self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
kwargs)
kwargs.pop('x')
rql, qargs, pending_relations, attrcache = self._cw_build_entity_query(kwargs)
if rql:
rql = 'SET ' + rql
qargs['x'] = self.eid
if ' WHERE ' in rql:
rql += ', X eid %(x)s'
else:
rql += ' WHERE X eid %(x)s'
self._cw.execute(rql, qargs)
# update current local object _after_ the rql query to avoid
# 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
is the object of the relation, use 'reverse_'<relation> as argument name.
Values may be an entity or eid, a list of entities or eids, or None
(meaning that all relations of the given type from or to this object
should be deleted).
"""
# XXX update cache
_check_cw_unsafe(kwargs)
for attr, values in kwargs.iteritems():
if attr.startswith('reverse_'):
restr = 'Y %s X' % attr[len('reverse_'):]
else:
restr = 'X %s Y' % attr
if values is None:
self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr,
{'x': self.eid})
continue
if not isinstance(values, (tuple, list, set, frozenset)):
values = (values,)
eids = []
for val in values:
try:
eids.append(str(val.eid))
except AttributeError:
try:
eids.append(str(typed_eid(val)))
except (ValueError, TypeError):
raise Exception('expected an Entity or eid, got %s' % val)
self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
restr, ','.join(eids)), {'x': self.eid})
self.cw_attr_cache.update(attrcache)
self._cw_handle_pending_relations(self.eid, pending_relations, self._cw.execute)
# XXX update relation cache
def cw_delete(self, **kwargs):
assert self.has_eid(), self.eid
......@@ -1274,6 +1283,21 @@ class Entity(AppObject):
# deprecated stuff #########################################################
@deprecated('[3.15] use cw_set() instead')
def set_attributes(self, **kwargs): # XXX cw_set_attributes
self.cw_set(**kwargs)
@deprecated('[3.15] use cw_set() instead')
def set_relations(self, **kwargs): # XXX cw_set_relations
"""add relations to the given object. To set a relation where this entity
is the object of the relation, use 'reverse_'<relation> as argument name.
Values may be an entity or eid, a list of entities or eids, or None
(meaning that all relations of the given type from or to this object
should be deleted).
"""
self.cw_set(**kwargs)
@deprecated('[3.13] use entity.cw_clear_all_caches()')
def clear_all_caches(self):
return self.cw_clear_all_caches()
......
......@@ -67,9 +67,9 @@ class CoreHooksTC(CubicWebTC):
entity = self.request().create_entity('Workflow', name=u'wf1',
description_format=u'text/html',
description=u'yo')
entity.set_attributes(name=u'wf2')
entity.cw_set(name=u'wf2')
self.assertEqual(entity.description, u'yo')
entity.set_attributes(description=u'R&D<p>yo')
entity.cw_set(description=u'R&D<p>yo')
entity.cw_attr_cache.pop('description')
self.assertEqual(entity.description, u'R&amp;D<p>yo</p>')
......
......@@ -294,7 +294,7 @@ class SchemaModificationHooksTC(CubicWebTC):
def test_change_fulltext_container(self):
req = self.request()
target = req.create_entity(u'EmailAddress', address=u'rick.roll@dance.com')
target.set_relations(reverse_use_email=req.user)
target.cw_set(reverse_use_email=req.user)
self.commit()
rset = req.execute('Any X WHERE X has_text "rick.roll"')
self.assertIn(req.user.eid, [item[0] for item in rset])
......
......@@ -335,7 +335,7 @@ class SetModificationDateOnStateChange(WorkflowHook):
return
entity = self._cw.entity_from_eid(self.eidfrom)
try:
entity.set_attributes(modification_date=datetime.now())
entity.cw_set(modification_date=datetime.now())
except RepositoryError, ex:
# usually occurs if entity is coming from a read-only source
# (eg ldap user)
......
......@@ -34,5 +34,5 @@ commit()
for x in rql('Any X,XK WHERE X pkey XK, '
'X pkey ~= "boxes.%" OR '
'X pkey ~= "contentnavigation.%"').entities():
x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
x.cw_set(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
......@@ -81,5 +81,5 @@ else:
rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s',
{'k': pkey})
timestamp = int(rset[0][0])
sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp))
sourceentity.cw_set(latest_retrieval=datetime.fromtimestamp(timestamp))
session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey})
......@@ -9,5 +9,5 @@ for rqlcstr in rql('Any X,XT,XV WHERE X is CWConstraint, X cstrtype XT, X value
expression = rqlcstr.value
mainvars = guess_rrqlexpr_mainvars(expression)
yamscstr = CONSTRAINTS[rqlcstr.type](expression, mainvars)
rqlcstr.set_attributes(value=yamscstr.serialize())
rqlcstr.cw_set(value=yamscstr.serialize())
print 'updated', rqlcstr.type, rqlcstr.value.strip()
......@@ -4,7 +4,7 @@ for source in rql('CWSource X WHERE X type "ldapuser"').entities():
config = source.dictconfig
host = config.pop('host', u'ldap')
protocol = config.pop('protocol', u'ldap')
source.set_attributes(url=u'%s://%s' % (protocol, host))
source.cw_set(url=u'%s://%s' % (protocol, host))
source.update_config(skip_unknown=True, **config)
commit()
......@@ -42,7 +42,7 @@ if pass1 != pass2:
crypted = crypt_password(pass1)
cwuser = rset.get_entity(0,0)
cwuser.set_attributes(upassword=Binary(crypted))
cwuser.cw_set(upassword=Binary(crypted))
commit()
print("password updated.")
......@@ -70,7 +70,7 @@ for entities in todelete.values():
source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed")
source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed")
commit()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment