--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/entity.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1403 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""Base class for entity objects manipulated in clients"""
+
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+from functools import partial
+
+from six import text_type, string_types, integer_types
+from six.moves import range
+
+from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
+from logilab.mtconverter import TransformData, xml_escape
+
+from rql.utils import rqlvar_maker
+from rql.stmts import Select
+from rql.nodes import (Not, VariableRef, Constant, make_relation,
+ Relation as RqlRelation)
+
+from cubicweb import Unauthorized, neg_role
+from cubicweb.utils import support_args
+from cubicweb.rset import ResultSet
+from cubicweb.appobject import AppObject
+from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
+ GeneratedConstraint)
+from cubicweb.rqlrewrite import RQLRewriter
+
+from cubicweb.uilib import soup2xhtml
+from cubicweb.mttransforms import ENGINE
+
+_marker = object()
+
+def greater_card(rschema, subjtypes, objtypes, index):
+ for subjtype in subjtypes:
+ for objtype in objtypes:
+ card = rschema.rdef(subjtype, objtype).cardinality[index]
+ if card in '+*':
+ return card
+ return '1'
+
+def can_use_rest_path(value):
+ """return True if value can be used at the end of a Rest URL path"""
+ if value is None:
+ return False
+ value = text_type(value)
+ # the check for ?, /, & are to prevent problems when running
+ # behind Apache mod_proxy
+ if value == u'' or u'?' in value or u'/' in value or u'&' in value:
+ return False
+ return True
+
+def rel_vars(rel):
+ return ((isinstance(rel.children[0], VariableRef)
+ and rel.children[0].variable or None),
+ (isinstance(rel.children[1].children[0], VariableRef)
+ and rel.children[1].children[0].variable or None)
+ )
+
+def rel_matches(rel, rtype, role, varname, operator='='):
+ if rel.r_type == rtype and rel.children[1].operator == operator:
+ same_role_var_idx = 0 if role == 'subject' else 1
+ variables = rel_vars(rel)
+ if variables[same_role_var_idx].name == varname:
+ return variables[1 - same_role_var_idx]
+
+def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
+ lt_infos, eidvars):
+ """restrict vocabulary as much as possible in entity creation,
+ based on infos provided by __linkto form param.
+
+ Example based on following schema:
+
+ class works_in(RelationDefinition):
+ subject = 'CWUser'
+ object = 'Lab'
+ cardinality = '1*'
+ constraints = [RQLConstraint('S in_group G, O welcomes G')]
+
+ class welcomes(RelationDefinition):
+ subject = 'Lab'
+ object = 'CWGroup'
+
+ If you create a CWUser in the "scientists" CWGroup you can show
+ only the labs that welcome them using :
+
+ lt_infos = {('in_group', 'subject'): 321}
+
+ You get following restriction : 'O welcomes G, G eid 321'
+
+ """
+ st = cstr.snippet_rqlst.copy()
+ # replace relations in ST by eid infos from linkto where possible
+ for (info_rtype, info_role), eids in lt_infos.items():
+ eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
+ for rel in st.iget_nodes(RqlRelation):
+ targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
+ if targetvar is not None:
+ if targetvar.name in eidvars:
+ rel.parent.remove(rel)
+ else:
+ eidrel = make_relation(
+ targetvar, 'eid', (targetvar.name, 'Substitute'),
+ Constant)
+ rel.parent.replace(rel, eidrel)
+ args[targetvar.name] = eid
+ eidvars.add(targetvar.name)
+ # if modified ST still contains evar references we must discard the
+ # constraint, otherwise evar is unknown in the final rql query which can
+ # lead to a SQL table cartesian product and multiple occurences of solutions
+ evarname = evar.name
+ for rel in st.iget_nodes(RqlRelation):
+ for variable in rel_vars(rel):
+ if variable and evarname == variable.name:
+ return
+ # else insert snippets into the global tree
+ return GeneratedConstraint(st, cstr.mainvars - set(evarname))
+
+def pruned_lt_info(eschema, lt_infos):
+ pruned = {}
+ for (lt_rtype, lt_role), eids in lt_infos.items():
+ # we can only use lt_infos describing relation with a cardinality
+ # of value 1 towards the linked entity
+ if not len(eids) == 1:
+ continue
+ lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
+ 0 if lt_role == 'subject' else 1]
+ if lt_card not in '?1':
+ continue
+ pruned[(lt_rtype, lt_role)] = eids
+ return pruned
+
+
+class Entity(AppObject):
+ """an entity instance has e_schema automagically set on
+ the class and instances has access to their issuing cursor.
+
+ A property is set for each attribute and relation on each entity's type
+ class. Becare that among attributes, 'eid' is *NEITHER* stored in the
+ dict containment (which acts as a cache for other attributes dynamically
+ fetched)
+
+ :type e_schema: `cubicweb.schema.EntitySchema`
+ :ivar e_schema: the entity's schema
+
+ :type rest_attr: str
+ :cvar rest_attr: indicates which attribute should be used to build REST urls
+ If `None` is specified (the default), the first unique attribute will
+ be used ('eid' if none found)
+
+ :type cw_skip_copy_for: list
+ :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
+ that should be skipped when copying this kind of entity. Note that some
+ relations such as composite relations or relations that have '?1' as
+ object cardinality are always skipped.
+ """
+ __registry__ = 'etypes'
+ __select__ = yes()
+
+ # class attributes that must be set in class definition
+ rest_attr = None
+ fetch_attrs = None
+ skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
+ cw_skip_copy_for = [('in_state', 'subject')]
+ # class attributes set automatically at registration time
+ e_schema = None
+
+ @classmethod
+ def __initialize__(cls, schema):
+ """initialize a specific entity class by adding descriptors to access
+ entity type's attributes and relations
+ """
+ etype = cls.__regid__
+ assert etype != 'Any', etype
+ cls.e_schema = eschema = schema.eschema(etype)
+ for rschema, _ in eschema.attribute_definitions():
+ if rschema.type == 'eid':
+ continue
+ setattr(cls, rschema.type, Attribute(rschema.type))
+ mixins = []
+ for rschema, _, role in eschema.relation_definitions():
+ if role == 'subject':
+ attr = rschema.type
+ else:
+ attr = 'reverse_%s' % rschema.type
+ setattr(cls, attr, Relation(rschema, role))
+
+ fetch_attrs = ('modification_date',)
+
+ @classmethod
+ def cw_fetch_order(cls, select, attr, var):
+ """This class method may be used to control sort order when multiple
+ entities of this type are fetched through ORM methods. Its arguments
+ are:
+
+ * `select`, the RQL syntax tree
+
+ * `attr`, the attribute being watched
+
+ * `var`, the variable through which this attribute's value may be
+ accessed in the query
+
+ When you want to do some sorting on the given attribute, you should
+ modify the syntax tree accordingly. For instance:
+
+ .. sourcecode:: python
+
+ from rql import nodes
+
+ class Version(AnyEntity):
+ __regid__ = 'Version'
+
+ fetch_attrs = ('num', 'description', 'in_state')
+
+ @classmethod
+ def cw_fetch_order(cls, select, attr, var):
+ if attr == 'num':
+ func = nodes.Function('version_sort_value')
+ func.append(nodes.variable_ref(var))
+ sterm = nodes.SortTerm(func, asc=False)
+ select.add_sort_term(sterm)
+
+ The default implementation call
+ :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
+ """
+ cls.cw_fetch_unrelated_order(select, attr, var)
+
+ @classmethod
+ def cw_fetch_unrelated_order(cls, select, attr, var):
+ """This class method may be used to control sort order when multiple entities of
+ this type are fetched to use in edition (e.g. propose them to create a
+ new relation on an edited entity).
+
+ See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
+ description of its arguments and usage.
+
+ By default entities will be listed on their modification date descending,
+ i.e. you'll get entities recently modified first.
+ """
+ if attr == 'modification_date':
+ select.add_sort_var(var, asc=False)
+
+ @classmethod
+ def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
+ settype=True, ordermethod='fetch_order'):
+ st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
+ settype=settype, ordermethod=ordermethod)
+ rql = st.as_string()
+ if restriction:
+ # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
+ warn('[3.14] fetch_rql: use of `restriction` parameter is '
+ 'deprecated, please use fetch_rqlst and supply a syntax'
+ 'tree with your restriction instead', DeprecationWarning)
+ insert = ' WHERE ' + ','.join(restriction)
+ if ' WHERE ' in rql:
+ select, where = rql.split(' WHERE ', 1)
+ rql = select + insert + ',' + where
+ else:
+ rql += insert
+ return rql
+
+ @classmethod
+ def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
+ settype=True, ordermethod='fetch_order'):
+ if select is None:
+ select = Select()
+ mainvar = select.get_variable(mainvar)
+ select.add_selected(mainvar)
+ elif isinstance(mainvar, string_types):
+ assert mainvar in select.defined_vars
+ mainvar = select.get_variable(mainvar)
+ # eases string -> syntax tree test transition: please remove once stable
+ select._varmaker = rqlvar_maker(defined=select.defined_vars,
+ aliases=select.aliases, index=26)
+ if settype:
+ rel = select.add_type_restriction(mainvar, cls.__regid__)
+ # should use 'is_instance_of' instead of 'is' so we retrieve
+ # subclasses instances as well
+ rel.r_type = 'is_instance_of'
+ if fetchattrs is None:
+ fetchattrs = cls.fetch_attrs
+ cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
+ return select
+
+ @classmethod
+ def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema):
+ """find rtypes in `fetchattrs` that relate different subject etypes
+ taken from (`subjtypes`) to different target etypes; these so called
+ "ambiguous" relations, are added directly to the `select` syntax tree
+ selection but removed from `fetchattrs` to avoid the fetch recursion
+ because we have to choose only one targettype for the recursion and
+ adding its own fetch attrs to the selection -when we recurse- would
+ filter out the other possible target types from the result set
+ """
+ for attr in fetchattrs.copy():
+ rschema = schema.rschema(attr)
+ if rschema.final:
+ continue
+ ttypes = None
+ for subjtype in subjtypes:
+ cur_ttypes = set(rschema.objects(subjtype))
+ if ttypes is None:
+ ttypes = cur_ttypes
+ elif cur_ttypes != ttypes:
+ # we found an ambiguous relation: remove it from fetchattrs
+ fetchattrs.remove(attr)
+ # ... and add it to the selection
+ targetvar = select.make_variable()
+ select.add_selected(targetvar)
+ rel = make_relation(var, attr, (targetvar,), VariableRef)
+ select.add_restriction(rel)
+ break
+
+ @classmethod
+ def _fetch_restrictions(cls, mainvar, select, fetchattrs,
+ user, ordermethod='fetch_order', visited=None):
+ eschema = cls.e_schema
+ if visited is None:
+ visited = set((eschema.type,))
+ elif eschema.type in visited:
+ # avoid infinite recursion
+ return
+ else:
+ visited.add(eschema.type)
+ _fetchattrs = []
+ for attr in sorted(fetchattrs):
+ try:
+ rschema = eschema.subjrels[attr]
+ except KeyError:
+ cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
+ attr, cls.__regid__)
+ continue
+ # XXX takefirst=True to remove warning triggered by ambiguous inlined relations
+ rdef = eschema.rdef(attr, takefirst=True)
+ if not user.matching_groups(rdef.get_groups('read')):
+ continue
+ if rschema.final or rdef.cardinality[0] in '?1':
+ var = select.make_variable()
+ select.add_selected(var)
+ rel = make_relation(mainvar, attr, (var,), VariableRef)
+ select.add_restriction(rel)
+ else:
+ cls.warning('bad relation %s specified in fetch attrs for %s',
+ attr, cls)
+ continue
+ if not rschema.final:
+ # XXX we need outer join in case the relation is not mandatory
+ # (card == '?') *or if the entity is being added*, since in
+ # that case the relation may still be missing. As we miss this
+ # later information here, systematically add it.
+ rel.change_optional('right')
+ targettypes = rschema.objects(eschema.type)
+ vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
+ etypecls = vreg['etypes'].etype_class(targettypes[0])
+ if len(targettypes) > 1:
+ # find fetch_attrs common to all destination types
+ fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
+ # ... and handle ambiguous relations
+ cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
+ targettypes, vreg.schema)
+ else:
+ fetchattrs = etypecls.fetch_attrs
+ etypecls._fetch_restrictions(var, select, fetchattrs,
+ user, None, visited=visited)
+ if ordermethod is not None:
+ try:
+ cmeth = getattr(cls, ordermethod)
+ warn('[3.14] %s %s class method should be renamed to cw_%s'
+ % (cls.__regid__, ordermethod, ordermethod),
+ DeprecationWarning)
+ except AttributeError:
+ cmeth = getattr(cls, 'cw_' + ordermethod)
+ if support_args(cmeth, 'select'):
+ cmeth(select, attr, var)
+ else:
+ warn('[3.14] %s should now take (select, attr, var) and '
+ 'modify the syntax tree when desired instead of '
+ 'returning something' % cmeth, DeprecationWarning)
+ orderterm = cmeth(attr, var.name)
+ if orderterm is not None:
+ try:
+ var, order = orderterm.split()
+ except ValueError:
+ if '(' in orderterm:
+ cls.error('ignore %s until %s is upgraded',
+ orderterm, cmeth)
+ orderterm = None
+ elif not ' ' in orderterm.strip():
+ var = orderterm
+ order = 'ASC'
+ if orderterm is not None:
+ select.add_sort_var(select.get_variable(var),
+ order=='ASC')
+
+ @classmethod
+ @cached
+ def cw_rest_attr_info(cls):
+ """this class method return an attribute name to be used in URL for
+ entities of this type and a boolean flag telling if its value should be
+ checked for uniqness.
+
+ The attribute returned is, in order of priority:
+
+ * class's `rest_attr` class attribute
+ * an attribute defined as unique in the class'schema
+ * 'eid'
+ """
+ mainattr, needcheck = 'eid', True
+ if cls.rest_attr:
+ mainattr = cls.rest_attr
+ needcheck = not cls.e_schema.has_unique_values(mainattr)
+ else:
+ for rschema in cls.e_schema.subject_relations():
+ if (rschema.final
+ and rschema not in ('eid', 'cwuri')
+ and cls.e_schema.has_unique_values(rschema)
+ and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
+ mainattr = str(rschema)
+ needcheck = False
+ break
+ if mainattr == 'eid':
+ needcheck = False
+ return mainattr, needcheck
+
+ @classmethod
+ def _cw_build_entity_query(cls, kwargs):
+ relations = []
+ restrictions = set()
+ pendingrels = []
+ eschema = cls.e_schema
+ qargs = {}
+ attrcache = {}
+ for attr, value in kwargs.items():
+ if attr.startswith('reverse_'):
+ attr = attr[len('reverse_'):]
+ role = 'object'
+ else:
+ role = 'subject'
+ assert eschema.has_relation(attr, role), '%s %s not found on %s' % (attr, role, eschema)
+ rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
+ if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
+ if len(value) == 0:
+ continue # avoid crash with empty IN clause
+ elif len(value) == 1:
+ value = next(iter(value))
+ else:
+ # prepare IN clause
+ pendingrels.append( (attr, role, value) )
+ continue
+ if rschema.final: # attribute
+ relations.append('X %s %%(%s)s' % (attr, attr))
+ attrcache[attr] = value
+ elif value is None:
+ pendingrels.append( (attr, role, value) )
+ else:
+ rvar = attr.upper()
+ if role == 'object':
+ relations.append('%s %s X' % (rvar, attr))
+ else:
+ relations.append('X %s %s' % (attr, rvar))
+ restriction = '%s eid %%(%s)s' % (rvar, attr)
+ if not restriction in restrictions:
+ restrictions.add(restriction)
+ if hasattr(value, 'eid'):
+ value = value.eid
+ qargs[attr] = value
+ rql = u''
+ if relations:
+ rql += ', '.join(relations)
+ if restrictions:
+ rql += ' WHERE %s' % ', '.join(restrictions)
+ return rql, qargs, pendingrels, attrcache
+
+ @classmethod
+ def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
+ for attr, role, values in pendingrels:
+ 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': 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, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+ if rql:
+ rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
+ else:
+ rql = 'INSERT %s X' % (cls.__regid__)
+ try:
+ created = execute(rql, qargs).get_entity(0, 0)
+ except IndexError:
+ raise Exception('could not create a %r with %r (%r)' %
+ (cls.__regid__, rql, qargs))
+ created._cw_update_attr_cache(attrcache)
+ cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
+ return created
+
+ def __init__(self, req, rset=None, row=None, col=0):
+ AppObject.__init__(self, req, rset=rset, row=row, col=col)
+ self._cw_related_cache = {}
+ self._cw_adapters_cache = {}
+ if rset is not None:
+ self.eid = rset[row][col]
+ else:
+ self.eid = None
+ self._cw_is_saved = True
+ self.cw_attr_cache = {}
+
+ def __repr__(self):
+ return '<Entity %s %s %s at %s>' % (
+ self.e_schema, self.eid, list(self.cw_attr_cache), id(self))
+
+ def __lt__(self, other):
+ raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
+ def __eq__(self, other):
+ if isinstance(self.eid, integer_types):
+ return self.eid == other.eid
+ return self is other
+
+ def __hash__(self):
+ if isinstance(self.eid, integer_types):
+ return self.eid
+ return super(Entity, self).__hash__()
+
+ def _cw_update_attr_cache(self, attrcache):
+ trdata = self._cw.transaction_data
+ uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
+ uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
+ for attr in uncached_attrs:
+ attrcache.pop(attr, None)
+ self.cw_attr_cache.pop(attr, None)
+ self.cw_attr_cache.update(attrcache)
+
+ def _cw_dont_cache_attribute(self, attr, repo_side=False):
+ """Called when some attribute has been transformed by a *storage*,
+ hence the original value should not be cached **by anyone**.
+
+ For example we have a special "fs_importing" mode in BFSS
+ where a file path is given as attribute value and stored as is
+ in the data base. Later access to the attribute will provide
+ the content of the file at the specified path. We do not want
+ the "filepath" value to be cached.
+
+ """
+ trdata = self._cw.transaction_data
+ trdata.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
+ if repo_side:
+ trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
+
+ def __json_encode__(self):
+ """custom json dumps hook to dump the entity's eid
+ which is not part of dict structure itself
+ """
+ dumpable = self.cw_attr_cache.copy()
+ dumpable['eid'] = self.eid
+ return dumpable
+
+ def cw_adapt_to(self, interface):
+ """return an adapter the entity to the given interface name.
+
+ return None if it can not be adapted.
+ """
+ cache = self._cw_adapters_cache
+ try:
+ return cache[interface]
+ except KeyError:
+ adapter = self._cw.vreg['adapters'].select_or_none(
+ interface, self._cw, entity=self)
+ cache[interface] = adapter
+ return adapter
+
+ def has_eid(self): # XXX cw_has_eid
+ """return True if the entity has an attributed eid (False
+ meaning that the entity has to be created
+ """
+ try:
+ int(self.eid)
+ return True
+ except (ValueError, TypeError):
+ return False
+
+ def cw_is_saved(self):
+ """during entity creation, there is some time during which the entity
+ has an eid attributed though it's not saved (eg during
+ 'before_add_entity' hooks). You can use this method to ensure the entity
+ has an eid *and* is saved in its source.
+ """
+ return self.has_eid() and self._cw_is_saved
+
+ @cached
+ def cw_metainformation(self):
+ metas = self._cw.entity_metas(self.eid)
+ metas['source'] = self._cw.source_defs()[metas['source']]
+ return metas
+
+ def cw_check_perm(self, action):
+ self.e_schema.check_perm(self._cw, action, eid=self.eid)
+
+ def cw_has_perm(self, action):
+ return self.e_schema.has_perm(self._cw, action, eid=self.eid)
+
+ def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
+ """shortcut to apply a view on this entity"""
+ if initargs is None:
+ initargs = kwargs
+ else:
+ initargs.update(kwargs)
+ view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
+ row=self.cw_row, col=self.cw_col,
+ **initargs)
+ return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
+
+ def absolute_url(self, *args, **kwargs): # XXX cw_url
+ """return an absolute url to view this entity"""
+ # use *args since we don't want first argument to be "anonymous" to
+ # avoid potential clash with kwargs
+ if args:
+ assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
+ method = args[0]
+ else:
+ method = None
+ # in linksearch mode, we don't want external urls else selecting
+ # the object for use in the relation is tricky
+ # XXX search_state is web specific
+ use_ext_id = False
+ if 'base_url' not in kwargs and \
+ getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
+ sourcemeta = self.cw_metainformation()['source']
+ if sourcemeta.get('use-cwuri-as-url'):
+ return self.cwuri # XXX consider kwargs?
+ if sourcemeta.get('base-url'):
+ kwargs['base_url'] = sourcemeta['base-url']
+ use_ext_id = True
+ if method in (None, 'view'):
+ kwargs['_restpath'] = self.rest_path(use_ext_id)
+ else:
+ kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
+ return self._cw.build_url(method, **kwargs)
+
+ def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
+ """returns a REST-like (relative) path for this entity"""
+ mainattr, needcheck = self.cw_rest_attr_info()
+ etype = str(self.e_schema)
+ path = etype.lower()
+ fallback = False
+ if mainattr != 'eid':
+ value = getattr(self, mainattr)
+ if not can_use_rest_path(value):
+ mainattr = 'eid'
+ path = None
+ elif needcheck:
+ # make sure url is not ambiguous
+ try:
+ nbresults = self.__unique
+ except AttributeError:
+ rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (
+ etype, mainattr)
+ nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
+ if nbresults != 1: # ambiguity?
+ mainattr = 'eid'
+ path = None
+ if mainattr == 'eid':
+ if use_ext_eid:
+ value = self.cw_metainformation()['extid']
+ else:
+ value = self.eid
+ if path is None:
+ # fallback url: <base-url>/<eid> url is used as cw entities uri,
+ # prefer it to <base-url>/<etype>/eid/<eid>
+ return text_type(value)
+ return u'%s/%s' % (path, self._cw.url_quote(value))
+
+ def cw_attr_metadata(self, attr, metadata):
+ """return a metadata for an attribute (None if unspecified)"""
+ value = getattr(self, '%s_%s' % (attr, metadata), None)
+ if value is None and metadata == 'encoding':
+ value = self._cw.vreg.property_value('ui.encoding')
+ return value
+
+ def printable_value(self, attr, value=_marker, attrtype=None,
+ format='text/html', displaytime=True): # XXX cw_printable_value
+ """return a displayable value (i.e. unicode string) which may contains
+ html tags
+ """
+ attr = str(attr)
+ if value is _marker:
+ value = getattr(self, attr)
+ if isinstance(value, string_types):
+ value = value.strip()
+ if value is None or value == '': # don't use "not", 0 is an acceptable value
+ return u''
+ if attrtype is None:
+ attrtype = self.e_schema.destination(attr)
+ props = self.e_schema.rdef(attr)
+ if attrtype == 'String':
+ # internalinalized *and* formatted string such as schema
+ # description...
+ if props.internationalizable:
+ value = self._cw._(value)
+ attrformat = self.cw_attr_metadata(attr, 'format')
+ if attrformat:
+ return self._cw_mtc_transform(value, attrformat, format,
+ self._cw.encoding)
+ elif attrtype == 'Bytes':
+ attrformat = self.cw_attr_metadata(attr, 'format')
+ if attrformat:
+ encoding = self.cw_attr_metadata(attr, 'encoding')
+ return self._cw_mtc_transform(value.getvalue(), attrformat, format,
+ encoding)
+ return u''
+ value = self._cw.printable_value(attrtype, value, props,
+ displaytime=displaytime)
+ if format == 'text/html':
+ value = xml_escape(value)
+ return value
+
+ def _cw_mtc_transform(self, data, format, target_format, encoding,
+ _engine=ENGINE):
+ trdata = TransformData(data, format, encoding, appobject=self)
+ data = _engine.convert(trdata, target_format).decode()
+ if target_format == 'text/html':
+ data = soup2xhtml(data, self._cw.encoding)
+ return data
+
+ # entity cloning ##########################################################
+
+ 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
+ ceid designates the original entity).
+
+ By default meta and composite relations are skipped.
+ Overrides this if you want another behaviour
+ """
+ assert self.has_eid()
+ execute = self._cw.execute
+ skip_copy_for = {'subject': set(), 'object': set()}
+ for rtype in self.skip_copy_for:
+ skip_copy_for['subject'].add(rtype)
+ warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
+ 'use cw_skip_for instead with list of couples (rtype, role)' % self.cw_etype,
+ DeprecationWarning)
+ for rtype, role in self.cw_skip_copy_for:
+ assert role in ('subject', 'object'), role
+ skip_copy_for[role].add(rtype)
+ for rschema in self.e_schema.subject_relations():
+ if rschema.type in skip_copy_for['subject']:
+ continue
+ if rschema.final or rschema.meta:
+ continue
+ # skip already defined relations
+ if getattr(self, rschema.type):
+ continue
+ # XXX takefirst=True to remove warning triggered by ambiguous relations
+ rdef = self.e_schema.rdef(rschema, takefirst=True)
+ # skip composite relation
+ if rdef.composite:
+ continue
+ # skip relation with card in ?1 else we either change the copied
+ # object (inlined relation) or inserting some inconsistency
+ if rdef.cardinality[1] in '?1':
+ continue
+ rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
+ rschema.type, rschema.type)
+ execute(rql, {'x': self.eid, 'y': ceid})
+ self.cw_clear_relation_cache(rschema.type, 'subject')
+ for rschema in self.e_schema.object_relations():
+ if rschema.meta:
+ continue
+ # skip already defined relations
+ if self.related(rschema.type, 'object'):
+ continue
+ if rschema.type in skip_copy_for['object']:
+ continue
+ # XXX takefirst=True to remove warning triggered by ambiguous relations
+ rdef = self.e_schema.rdef(rschema, 'object', takefirst=True)
+ # skip composite relation
+ if rdef.composite:
+ continue
+ # skip relation with card in ?1 else we either change the copied
+ # object (inlined relation) or inserting some inconsistency
+ if rdef.cardinality[0] in '?1':
+ continue
+ rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
+ rschema.type, rschema.type)
+ execute(rql, {'x': self.eid, 'y': ceid})
+ self.cw_clear_relation_cache(rschema.type, 'object')
+
+ # data fetching methods ###################################################
+
+ @cached
+ def as_rset(self): # XXX .cw_as_rset
+ """returns a resultset containing `self` information"""
+ rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
+ {'x': self.eid}, [(self.cw_etype,)])
+ rset.req = self._cw
+ return rset
+
+ def _cw_to_complete_relations(self):
+ """by default complete final relations to when calling .complete()"""
+ for rschema in self.e_schema.subject_relations():
+ if rschema.final:
+ continue
+ targets = rschema.objects(self.e_schema)
+ if rschema.inlined:
+ matching_groups = self._cw.user.matching_groups
+ if all(matching_groups(e.get_groups('read')) and
+ rschema.rdef(self.e_schema, e).get_groups('read')
+ for e in targets):
+ yield rschema, 'subject'
+
+ def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
+ for rschema, attrschema in self.e_schema.attribute_definitions():
+ # skip binary data by default
+ if skip_bytes and attrschema.type == 'Bytes':
+ continue
+ attr = rschema.type
+ if attr == 'eid':
+ continue
+ # password retrieval is blocked at the repository server level
+ 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.cw_attr_cache[attr] = None
+ continue
+ yield attr
+
+ _cw_completed = False
+ def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
+ """complete this entity by adding missing attributes (i.e. query the
+ repository to fill the entity)
+
+ :type skip_bytes: bool
+ :param skip_bytes:
+ if true, attribute of type Bytes won't be considered
+ """
+ assert self.has_eid()
+ if self._cw_completed:
+ return
+ if attributes is None:
+ self._cw_completed = True
+ varmaker = rqlvar_maker()
+ V = next(varmaker)
+ rql = ['WHERE %s eid %%(x)s' % V]
+ selected = []
+ for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
+ # if attribute already in entity, nothing to do
+ if attr in self.cw_attr_cache:
+ continue
+ # case where attribute must be completed, but is not yet in entity
+ var = next(varmaker)
+ rql.append('%s %s %s' % (V, attr, var))
+ selected.append((attr, var))
+ # +1 since this doesn't include the main variable
+ lastattr = len(selected) + 1
+ # don't fetch extra relation if attributes specified or of the entity is
+ # coming from an external source (may lead to error)
+ if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
+ # fetch additional relations (restricted to 0..1 relations)
+ for rschema, role in self._cw_to_complete_relations():
+ rtype = rschema.type
+ if self.cw_relation_cached(rtype, role):
+ continue
+ # at this point we suppose that:
+ # * this is a inlined relation
+ # * entity (self) is the subject
+ # * user has read perm on the relation and on the target entity
+ assert rschema.inlined
+ assert role == 'subject'
+ var = next(varmaker)
+ # keep outer join anyway, we don't want .complete to crash on
+ # missing mandatory relation (see #1058267)
+ rql.append('%s %s %s?' % (V, rtype, var))
+ selected.append(((rtype, role), var))
+ if selected:
+ # select V, we need it as the left most selected variable
+ # if some outer join are included to fetch inlined relations
+ rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
+ ','.join(rql))
+ try:
+ rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
+ except IndexError:
+ raise Exception('unable to fetch attributes for entity with eid %s'
+ % self.eid)
+ # handle attributes
+ for i in range(1, lastattr):
+ self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
+ # handle relations
+ for i in range(lastattr, len(rset)):
+ rtype, role = selected[i-1][0]
+ value = rset[i]
+ if value is None:
+ rrset = ResultSet([], rql, {'x': self.eid})
+ rrset.req = self._cw
+ else:
+ rrset = self._cw.eid_rset(value)
+ self.cw_set_relation_cache(rtype, role, rrset)
+
+ def cw_attr_value(self, name):
+ """get value for the attribute relation <name>, query the repository
+ to get the value if necessary.
+
+ :type name: str
+ :param name: name of the attribute to get
+ """
+ try:
+ return self.cw_attr_cache[name]
+ except KeyError:
+ if not self.cw_is_saved():
+ return None
+ rql = "Any A WHERE X eid %%(x)s, X %s A" % name
+ try:
+ rset = self._cw.execute(rql, {'x': self.eid})
+ except Unauthorized:
+ self.cw_attr_cache[name] = value = None
+ else:
+ assert rset.rowcount <= 1, (self, rql, rset.rowcount)
+ try:
+ 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':
+ self.cw_attr_cache[name] = value = self._cw._('unaccessible')
+ else:
+ self.cw_attr_cache[name] = value = None
+ return value
+
+ def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related
+ safe=False, targettypes=None):
+ """returns a resultset of related entities
+
+ :param rtype:
+ the name of the relation, aka relation type
+ :param role:
+ the role played by 'self' in the relation ('subject' or 'object')
+ :param limit:
+ resultset's maximum size
+ :param entities:
+ if True, the entites are returned; if False, a result set is returned
+ :param safe:
+ if True, an empty rset/list of entities will be returned in case of
+ :exc:`Unauthorized`, else (the default), the exception is propagated
+ :param targettypes:
+ a tuple of target entity types to restrict the query
+ """
+ rtype = str(rtype)
+ # Caching restricted/limited results is best avoided.
+ cacheable = limit is None and targettypes is None
+ if cacheable:
+ cache_key = '%s_%s' % (rtype, role)
+ if cache_key in self._cw_related_cache:
+ return self._cw_related_cache[cache_key][entities]
+ if not self.has_eid():
+ if entities:
+ return []
+ return self._cw.empty_rset()
+ rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes)
+ try:
+ rset = self._cw.execute(rql, {'x': self.eid})
+ except Unauthorized:
+ if not safe:
+ raise
+ rset = self._cw.empty_rset()
+ if entities:
+ if cacheable:
+ self.cw_set_relation_cache(rtype, role, rset)
+ return self.related(rtype, role, entities=entities)
+ return list(rset.entities())
+ else:
+ return rset
+
+ def cw_related_rql(self, rtype, role='subject', targettypes=None, limit=None):
+ vreg = self._cw.vreg
+ rschema = vreg.schema[rtype]
+ select = Select()
+ mainvar, evar = select.get_variable('X'), select.get_variable('E')
+ select.add_selected(mainvar)
+ if limit is not None:
+ select.set_limit(limit)
+ select.add_eid_restriction(evar, 'x', 'Substitute')
+ if role == 'subject':
+ rel = make_relation(evar, rtype, (mainvar,), VariableRef)
+ select.add_restriction(rel)
+ if targettypes is None:
+ targettypes = rschema.objects(self.e_schema)
+ else:
+ select.add_constant_restriction(mainvar, 'is',
+ targettypes, 'etype')
+ gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
+ else:
+ rel = make_relation(mainvar, rtype, (evar,), VariableRef)
+ select.add_restriction(rel)
+ if targettypes is None:
+ targettypes = rschema.subjects(self.e_schema)
+ else:
+ select.add_constant_restriction(mainvar, 'is', targettypes,
+ 'etype')
+ gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
+ etypecls = vreg['etypes'].etype_class(targettypes[0])
+ if len(targettypes) > 1:
+ fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
+ self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
+ targettypes, vreg.schema)
+ else:
+ fetchattrs = etypecls.fetch_attrs
+ etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
+ settype=False)
+ # optimisation: remove ORDERBY if cardinality is 1 or ? (though
+ # greater_card return 1 for those both cases)
+ if gcard == '1':
+ select.remove_sort_terms()
+ elif not select.orderby:
+ # if modification_date is already retrieved, we use it instead
+ # of adding another variable for sorting. This should not be
+ # problematic, but it is with sqlserver, see ticket #694445
+ for rel in select.where.get_nodes(RqlRelation):
+ if (rel.r_type == 'modification_date'
+ and rel.children[0].variable == mainvar
+ and rel.children[1].operator == '='):
+ var = rel.children[1].children[0].variable
+ select.add_sort_var(var, asc=False)
+ break
+ else:
+ mdvar = select.make_variable()
+ rel = make_relation(mainvar, 'modification_date',
+ (mdvar,), VariableRef)
+ select.add_restriction(rel)
+ select.add_sort_var(mdvar, asc=False)
+ return select.as_string()
+
+ # generic vocabulary methods ##############################################
+
+ def cw_linkable_rql(self, rtype, targettype, role, ordermethod=None,
+ vocabconstraints=True, lt_infos={}, limit=None):
+ """build a rql to fetch targettype entities either related or unrelated
+ to this entity using (rtype, role) relation.
+
+ Consider relation permissions so that returned entities may be actually
+ linked by `rtype`.
+
+ `lt_infos` are supplementary informations, usually coming from __linkto
+ parameter, that can help further restricting the results in case current
+ entity is not yet created. It is a dict describing entities the current
+ entity will be linked to, which keys are (rtype, role) tuples and values
+ are a list of eids.
+ """
+ return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
+ vocabconstraints=vocabconstraints,
+ lt_infos=lt_infos, limit=limit,
+ unrelated_only=False)
+
+ def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
+ vocabconstraints=True, lt_infos={}, limit=None):
+ """build a rql to fetch `targettype` entities unrelated to this entity
+ using (rtype, role) relation.
+
+ Consider relation permissions so that returned entities may be actually
+ linked by `rtype`.
+
+ `lt_infos` are supplementary informations, usually coming from __linkto
+ parameter, that can help further restricting the results in case current
+ entity is not yet created. It is a dict describing entities the current
+ entity will be linked to, which keys are (rtype, role) tuples and values
+ are a list of eids.
+ """
+ return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
+ vocabconstraints=vocabconstraints,
+ lt_infos=lt_infos, limit=limit,
+ unrelated_only=True)
+
+ def _cw_compute_linkable_rql(self, rtype, targettype, role, ordermethod=None,
+ vocabconstraints=True, lt_infos={}, limit=None,
+ unrelated_only=False):
+ """build a rql to fetch `targettype` entities that may be related to
+ this entity using the (rtype, role) relation.
+
+ By default (unrelated_only=False), this includes the already linked
+ entities as well as the unrelated ones. If `unrelated_only` is True, the
+ rql filters out the already related entities.
+ """
+ ordermethod = ordermethod or 'fetch_unrelated_order'
+ rschema = self._cw.vreg.schema.rschema(rtype)
+ rdef = rschema.role_rdef(self.e_schema, targettype, role)
+ rewriter = RQLRewriter(self._cw)
+ select = Select()
+ # initialize some variables according to the `role` of `self` in the
+ # relation (variable names must respect constraints conventions):
+ # * variable for myself (`evar`)
+ # * variable for searched entities (`searchvedvar`)
+ if role == 'subject':
+ evar = subjvar = select.get_variable('S')
+ searchedvar = objvar = select.get_variable('O')
+ else:
+ searchedvar = subjvar = select.get_variable('S')
+ evar = objvar = select.get_variable('O')
+ select.add_selected(searchedvar)
+ if limit is not None:
+ select.set_limit(limit)
+ # initialize some variables according to `self` existence
+ if rdef.role_cardinality(neg_role(role)) in '?1':
+ # if cardinality in '1?', we want a target entity which isn't
+ # already linked using this relation
+ variable = select.make_variable()
+ if role == 'subject':
+ rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
+ else:
+ rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
+ select.add_restriction(Not(rel))
+ elif self.has_eid() and unrelated_only:
+ # elif we have an eid, we don't want a target entity which is
+ # already linked to ourself through this relation
+ rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
+ select.add_restriction(Not(rel))
+ if self.has_eid():
+ rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
+ select.add_restriction(rel)
+ args = {'x': self.eid}
+ if role == 'subject':
+ sec_check_args = {'fromeid': self.eid}
+ else:
+ sec_check_args = {'toeid': self.eid}
+ existant = None # instead of 'SO', improve perfs
+ else:
+ args = {}
+ sec_check_args = {}
+ existant = searchedvar.name
+ # undefine unused evar, or the type resolver will consider it
+ select.undefine_variable(evar)
+ # retrieve entity class for targettype to compute base rql
+ etypecls = self._cw.vreg['etypes'].etype_class(targettype)
+ etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
+ ordermethod=ordermethod)
+ # from now on, we need variable type resolving
+ self._cw.vreg.solutions(self._cw, select, args)
+ # insert RQL expressions for schema constraints into the rql syntax tree
+ if vocabconstraints:
+ cstrcls = (RQLVocabularyConstraint, RQLConstraint)
+ else:
+ cstrcls = RQLConstraint
+ lt_infos = pruned_lt_info(self.e_schema, lt_infos or {})
+ # if there are still lt_infos, use set to keep track of added eid
+ # relations (adding twice the same eid relation is incorrect RQL)
+ eidvars = set()
+ for cstr in rdef.constraints:
+ # consider constraint.mainvars to check if constraint apply
+ if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
+ if not self.has_eid():
+ if lt_infos:
+ # we can perhaps further restrict with linkto infos using
+ # a custom constraint built from cstr and lt_infos
+ cstr = build_cstr_with_linkto_infos(
+ cstr, args, searchedvar, evar, lt_infos, eidvars)
+ if cstr is None:
+ continue # could not build constraint -> discard
+ elif evar.name in cstr.mainvars:
+ continue
+ # compute a varmap suitable to RQLRewriter.rewrite argument
+ varmap = dict((v, v) for v in (searchedvar.name, evar.name)
+ if v in select.defined_vars and v in cstr.mainvars)
+ # rewrite constraint by constraint since we want a AND between
+ # expressions.
+ rewriter.rewrite(select, [(varmap, (cstr,))], args, existant)
+ # insert security RQL expressions granting the permission to 'add' the
+ # relation into the rql syntax tree, if necessary
+ rqlexprs = rdef.get_rqlexprs('add')
+ if not self.has_eid():
+ rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+ if searchedvar.name in rqlexpr.mainvars]
+ if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
+ # compute a varmap suitable to RQLRewriter.rewrite argument
+ varmap = dict((v, v) for v in (searchedvar.name, evar.name)
+ if v in select.defined_vars)
+ # rewrite all expressions at once since we want a OR between them.
+ rewriter.rewrite(select, [(varmap, rqlexprs)], args, existant)
+ # ensure we have an order defined
+ if not select.orderby:
+ select.add_sort_var(select.defined_vars[searchedvar.name])
+ # we're done, turn the rql syntax tree as a string
+ rql = select.as_string()
+ return rql, args
+
+ def unrelated(self, rtype, targettype, role='subject', limit=None,
+ ordermethod=None, lt_infos={}): # XXX .cw_unrelated
+ """return a result set of target type objects that may be related
+ by a given relation, with self as subject or object
+ """
+ try:
+ rql, args = self.cw_unrelated_rql(rtype, targettype, role, limit=limit,
+ ordermethod=ordermethod, lt_infos=lt_infos)
+ except Unauthorized:
+ return self._cw.empty_rset()
+ return self._cw.execute(rql, args)
+
+ # relations cache handling #################################################
+
+ def cw_relation_cached(self, rtype, role):
+ """return None if the given relation isn't already cached on the
+ instance, else the content of the cache (a 2-uple (rset, entities)).
+ """
+ return self._cw_related_cache.get('%s_%s' % (rtype, role))
+
+ def cw_set_relation_cache(self, rtype, role, rset):
+ """set cached values for the given relation"""
+ if rset:
+ related = list(rset.entities(0))
+ rschema = self._cw.vreg.schema.rschema(rtype)
+ if role == 'subject':
+ rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
+ target = 'object'
+ else:
+ rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
+ target = 'subject'
+ if rcard in '?1':
+ for rentity in related:
+ rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
+ self.as_rset(), (self,))
+ else:
+ related = ()
+ self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
+
+ def cw_clear_relation_cache(self, rtype=None, role=None):
+ """clear cached values for the given relation or the entire cache if
+ no relation is given
+ """
+ if rtype is None:
+ self._cw_related_cache.clear()
+ self._cw_adapters_cache.clear()
+ else:
+ assert role
+ self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
+
+ def cw_clear_all_caches(self):
+ """flush all caches on this entity. Further attributes/relations access
+ will triggers new database queries to get back values.
+
+ If you use custom caches on your entity class (take care to @cached!),
+ you should override this method to clear them as well.
+ """
+ # clear attributes cache
+ self._cw_completed = False
+ self.cw_attr_cache.clear()
+ # clear relations cache
+ self.cw_clear_relation_cache()
+ # rest path unique cache
+ try:
+ del self.__unique
+ except AttributeError:
+ pass
+
+ # raw edition utilities ###################################################
+
+ 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.cw_set(name=u'Logilab')
+ >>> p.cw_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).
+ """
+ assert kwargs
+ assert self.cw_is_saved(), "should not call set_attributes while entity "\
+ "hasn't been saved yet"
+ rql, qargs, pendingrels, 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_update_attr_cache(attrcache)
+ self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
+ # XXX update relation cache
+
+ def cw_delete(self, **kwargs):
+ assert self.has_eid(), self.eid
+ self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
+ {'x': self.eid}, **kwargs)
+
+ # 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)
+
+ # deprecated stuff #########################################################
+
+ @deprecated('[3.16] use cw_set() instead of set_attributes()')
+ def set_attributes(self, **kwargs): # XXX cw_set_attributes
+ if kwargs:
+ self.cw_set(**kwargs)
+
+ @deprecated('[3.16] use cw_set() instead of set_relations()')
+ 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).
+ """
+ if kwargs:
+ self.cw_set(**kwargs)
+
+ @deprecated('[3.13] use entity.cw_clear_all_caches()')
+ def clear_all_caches(self):
+ return self.cw_clear_all_caches()
+
+
+# attribute and relation descriptors ##########################################
+
+class Attribute(object):
+ """descriptor that controls schema attribute access"""
+
+ def __init__(self, attrname):
+ assert attrname != 'eid'
+ self._attrname = attrname
+
+ def __get__(self, eobj, eclass):
+ if eobj is None:
+ return self
+ return eobj.cw_attr_value(self._attrname)
+
+ @deprecated('[3.10] assign to entity.cw_attr_cache[attr] or entity.cw_edited[attr]')
+ def __set__(self, eobj, value):
+ if hasattr(eobj, 'cw_edited') and not eobj.cw_edited.saved:
+ eobj.cw_edited[self._attrname] = value
+ else:
+ eobj.cw_attr_cache[self._attrname] = value
+
+
+class Relation(object):
+ """descriptor that controls schema relation access"""
+
+ def __init__(self, rschema, role):
+ self._rtype = rschema.type
+ self._role = role
+
+ def __get__(self, eobj, eclass):
+ if eobj is None:
+ raise AttributeError('%s can only be accessed from instances'
+ % self._rtype)
+ return eobj.related(self._rtype, self._role, entities=True)
+
+ def __set__(self, eobj, value):
+ raise NotImplementedError
+
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(Entity, getLogger('cubicweb.entity'))