--- a/entity.py Thu Dec 08 14:29:48 2011 +0100
+++ b/entity.py Fri Dec 09 12:08:44 2011 +0100
@@ -27,16 +27,21 @@
from logilab.mtconverter import TransformData, TransformError, 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, typed_eid, neg_role
+from cubicweb.utils import support_args
from cubicweb.rset import ResultSet
from cubicweb.selectors import yes
from cubicweb.appobject import AppObject
from cubicweb.req import _check_cw_unsafe
-from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
+from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
+ GeneratedConstraint)
from cubicweb.rqlrewrite import RQLRewriter
-from cubicweb.uilib import printable_value, soup2xhtml
+from cubicweb.uilib import soup2xhtml
from cubicweb.mixins import MI_REL_TRIGGERS
from cubicweb.mttransforms import ENGINE
@@ -61,23 +66,85 @@
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 remove_ambiguous_rels(attr_set, subjtypes, schema):
- '''remove from `attr_set` the relations of entity types `subjtypes` that have
- different entity type sets as target'''
- for attr in attr_set.copy():
- rschema = schema.rschema(attr)
- if rschema.final:
+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.iteritems():
+ 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.iteritems():
+ # we can only use lt_infos describing relation with a cardinality
+ # of value 1 towards the linked entity
+ if not len(eids) == 1:
continue
- ttypes = None
- for subjtype in subjtypes:
- cur_ttypes = rschema.objects(subjtype)
- if ttypes is None:
- ttypes = cur_ttypes
- elif cur_ttypes != ttypes:
- attr_set.remove(attr)
- break
-
+ 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
@@ -91,16 +158,16 @@
:type e_schema: `cubicweb.schema.EntitySchema`
:ivar e_schema: the entity's schema
- :type rest_var: str
- :cvar rest_var: indicates which attribute should be used to build REST urls
- If None is specified, the first non-meta attribute will
- be used
+ :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 skip_copy_for: list
- :cvar skip_copy_for: a list of relations 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.
+ :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()
@@ -108,7 +175,8 @@
# class attributes that must be set in class definition
rest_attr = None
fetch_attrs = None
- skip_copy_for = ('in_state',) # XXX turn into a set
+ 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
@@ -153,50 +221,131 @@
cls.info('plugged %s mixins on %s', mixins, cls)
fetch_attrs = ('modification_date',)
- @classmethod
- def fetch_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched
- """
- return cls.fetch_unrelated_order(attr, var)
@classmethod
- def fetch_unrelated_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched to use in edition (eg propose them to create a
+ 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':
- return '%s DESC' % var
- return None
+ select.add_sort_var(var, asc=False)
@classmethod
def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
settype=True, ordermethod='fetch_order'):
- """return a rql to fetch all entities of the class type"""
- # XXX update api and implementation to AST manipulation (see unrelated rql)
- restrictions = restriction or []
- if settype:
- restrictions.append('%s is %s' % (mainvar, cls.__regid__))
- if fetchattrs is None:
- fetchattrs = cls.fetch_attrs
- selection = [mainvar]
- orderby = []
- # start from 26 to avoid possible conflicts with X
- # XXX not enough to be sure it'll be no conflicts
- varmaker = rqlvar_maker(index=26)
- cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
- orderby, restrictions, user, ordermethod)
- rql = 'Any %s' % ','.join(selection)
- if orderby:
- rql += ' ORDERBY %s' % ','.join(orderby)
- rql += ' WHERE %s' % ', '.join(restrictions)
+ 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_restrictions(cls, mainvar, varmaker, fetchattrs,
- selection, orderby, restrictions, user,
- ordermethod='fetch_order', visited=None):
+ 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, basestring):
+ 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:
+ select.add_type_restriction(mainvar, cls.__regid__)
+ 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,))
@@ -216,51 +365,85 @@
rdef = eschema.rdef(attr)
if not user.matching_groups(rdef.get_groups('read')):
continue
- var = varmaker.next()
- selection.append(var)
- restriction = '%s %s %s' % (mainvar, attr, var)
- restrictions.append(restriction)
+ 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:
- card = rdef.cardinality[0]
- if card not in '?1':
- cls.warning('bad relation %s specified in fetch attrs for %s',
- attr, cls)
- selection.pop()
- restrictions.pop()
- continue
# 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.
- restrictions[-1] += '?'
+ rel.change_optional('right')
targettypes = rschema.objects(eschema.type)
- # XXX user._cw.vreg iiiirk
- etypecls = user._cw.vreg['etypes'].etype_class(targettypes[0])
+ 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 = user._cw.vreg['etypes'].fetch_attrs(targettypes)
- remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema)
+ 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, varmaker, fetchattrs,
- selection, orderby, restrictions,
+ etypecls._fetch_restrictions(var, select, fetchattrs,
user, ordermethod, visited=visited)
if ordermethod is not None:
- orderterm = getattr(cls, ordermethod)(attr, var)
- if orderterm:
- orderby.append(orderterm)
- return selection, orderby, restrictions
+ 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 _rest_attr_info(cls):
+ 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 != 'eid' and cls.e_schema.has_unique_values(rschema):
+ if rschema.final and rschema != 'eid' \
+ and cls.e_schema.has_unique_values(rschema):
mainattr = str(rschema)
needcheck = False
break
@@ -354,7 +537,7 @@
"""custom json dumps hook to dump the entity's eid
which is not part of dict structure itself
"""
- dumpable = dict(self)
+ dumpable = self.cw_attr_cache.copy()
dumpable['eid'] = self.eid
return dumpable
@@ -440,19 +623,14 @@
kwargs['base_url'] = sourcemeta['base-url']
use_ext_id = True
if method in (None, 'view'):
- try:
- kwargs['_restpath'] = self.rest_path(use_ext_id)
- except TypeError:
- warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
- 'please update' % self.__regid__, DeprecationWarning)
- kwargs['_restpath'] = self.rest_path()
+ 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._rest_attr_info()
+ mainattr, needcheck = self.cw_rest_attr_info()
etype = str(self.e_schema)
path = etype.lower()
if mainattr != 'eid':
@@ -516,8 +694,8 @@
return self._cw_mtc_transform(value.getvalue(), attrformat, format,
encoding)
return u''
- value = printable_value(self._cw, attrtype, value, props,
- displaytime=displaytime)
+ value = self._cw.printable_value(attrtype, value, props,
+ displaytime=displaytime)
if format == 'text/html':
value = xml_escape(value)
return value
@@ -542,13 +720,22 @@
"""
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.__regid__,
+ 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.final or rschema.meta:
continue
# skip already defined relations
if getattr(self, rschema.type):
continue
- if rschema.type in self.skip_copy_for:
+ if rschema.type in skip_copy_for['subject']:
continue
# skip composite relation
rdef = self.e_schema.rdef(rschema)
@@ -568,6 +755,8 @@
# skip already defined relations
if self.related(rschema.type, 'object'):
continue
+ if rschema.type in skip_copy_for['object']:
+ continue
rdef = self.e_schema.rdef(rschema, 'object')
# skip composite relation
if rdef.composite:
@@ -646,7 +835,7 @@
var = varmaker.next()
rql.append('%s %s %s' % (V, attr, var))
selected.append((attr, var))
- # +1 since this doen't include the main variable
+ # +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)
@@ -738,6 +927,7 @@
if True, an empty rset/list of entities will be returned in case of
:exc:`Unauthorized`, else (the default), the exception is propagated
"""
+ rtype = str(rtype)
try:
return self._cw_relation_cache(rtype, role, entities, limit)
except KeyError:
@@ -757,94 +947,112 @@
return self.related(rtype, role, limit, entities)
def cw_related_rql(self, rtype, role='subject', targettypes=None):
- rschema = self._cw.vreg.schema[rtype]
+ vreg = self._cw.vreg
+ rschema = vreg.schema[rtype]
+ select = Select()
+ mainvar, evar = select.get_variable('X'), select.get_variable('E')
+ select.add_selected(mainvar)
+ select.add_eid_restriction(evar, 'x', 'Substitute')
if role == 'subject':
- restriction = 'E eid %%(x)s, E %s X' % rtype
+ rel = make_relation(evar, rtype, (mainvar,), VariableRef)
+ select.add_restriction(rel)
if targettypes is None:
targettypes = rschema.objects(self.e_schema)
else:
- restriction += ', X is IN (%s)' % ','.join(targettypes)
- card = greater_card(rschema, (self.e_schema,), targettypes, 0)
+ select.add_constant_restriction(mainvar, 'is',
+ targettypes, 'etype')
+ gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
else:
- restriction = 'E eid %%(x)s, X %s E' % rtype
+ rel = make_relation(mainvar, rtype, (evar,), VariableRef)
+ select.add_restriction(rel)
if targettypes is None:
targettypes = rschema.subjects(self.e_schema)
else:
- restriction += ', X is IN (%s)' % ','.join(targettypes)
- card = greater_card(rschema, targettypes, (self.e_schema,), 1)
- etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
+ 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 = self._cw.vreg['etypes'].fetch_attrs(targettypes)
- # XXX we should fetch ambiguous relation objects too but not
- # recurse on them in _fetch_restrictions; it is easier to remove
- # them completely for now, as it would require an deeper api rewrite
- remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema)
+ fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
+ self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
+ targettypes, vreg.schema)
else:
fetchattrs = etypecls.fetch_attrs
- rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
- settype=False)
+ 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 card == '1':
- if ' ORDERBY ' in rql:
- rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
- rql.split(' WHERE ', 1)[1])
- elif not ' ORDERBY ' in rql:
- args = rql.split(' WHERE ', 1)
- # if modification_date already retrieved, we should use it instead
- # of adding another variable for sort. This should be be problematic
- # but it's actually with sqlserver, see ticket #694445
- if 'X modification_date ' in args[1]:
- var = args[1].split('X modification_date ', 1)[1].split(',', 1)[0]
- args.insert(1, var.strip())
- rql = '%s ORDERBY %s DESC WHERE %s' % tuple(args)
+ 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:
- rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % \
- tuple(args)
- return rql
+ 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_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
- vocabconstraints=True):
+ vocabconstraints=True, lt_infos={}):
"""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.
"""
ordermethod = ordermethod or 'fetch_unrelated_order'
- if isinstance(rtype, basestring):
- rtype = self._cw.vreg.schema.rschema(rtype)
- rdef = rtype.role_rdef(self.e_schema, targettype, role)
+ 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 for myself (`evar`) and searched entities (`searchvedvar`)
- # * entity type of the subject (`subjtype`) and of the object
- # (`objtype`) of the relation
+ # relation (variable names must respect constraints conventions):
+ # * variable for myself (`evar`)
+ # * variable for searched entities (`searchvedvar`)
if role == 'subject':
- evar, searchedvar = 'S', 'O'
- subjtype, objtype = self.e_schema, targettype
+ evar = subjvar = select.get_variable('S')
+ searchedvar = objvar = select.get_variable('O')
else:
- searchedvar, evar = 'S', 'O'
- objtype, subjtype = self.e_schema, targettype
- # initialize some variables according to `self` existance
+ searchedvar = subjvar = select.get_variable('S')
+ evar = objvar = select.get_variable('O')
+ select.add_selected(searchedvar)
+ # 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
- if searchedvar == 'S':
- restriction = ['NOT S %s ZZ' % rtype]
+ variable = select.make_variable()
+ if role == 'subject':
+ rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
else:
- restriction = ['NOT ZZ %s O' % rtype]
+ rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
+ select.add_restriction(Not(rel))
elif self.has_eid():
# elif we have an eid, we don't want a target entity which is
# already linked to ourself through this relation
- restriction = ['NOT S %s O' % rtype]
- else:
- restriction = []
+ rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
+ select.add_restriction(Not(rel))
if self.has_eid():
- restriction += ['%s eid %%(x)s' % evar]
+ 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}
@@ -854,12 +1062,15 @@
else:
args = {}
sec_check_args = {}
- existant = searchedvar
- # retreive entity class for targettype to compute base rql
+ 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)
- rql = etypecls.fetch_rql(self._cw.user, restriction,
- mainvar=searchedvar, ordermethod=ordermethod)
- select = self._cw.vreg.parse(self._cw, rql, args).children[0]
+ 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:
# RQLConstraint is a subclass for RQLVocabularyConstraint, so they
@@ -867,14 +1078,26 @@
cstrcls = RQLVocabularyConstraint
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 in cstr.mainvars:
- if not self.has_eid() and evar in cstr.mainvars:
- continue
+ 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 'SO' if v in select.defined_vars
- and v in cstr.mainvars)
+ 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,))], select.solutions,
@@ -884,24 +1107,26 @@
rqlexprs = rdef.get_rqlexprs('add')
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 'SO' if v in select.defined_vars)
+ 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)], select.solutions,
args, existant)
# ensure we have an order defined
if not select.orderby:
- select.add_sort_var(select.defined_vars[searchedvar])
+ 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): # XXX .cw_unrelated
+ 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, ordermethod)
+ rql, args = self.cw_unrelated_rql(rtype, targettype, role,
+ ordermethod, lt_infos=lt_infos)
except Unauthorized:
return self._cw.empty_rset()
# XXX should be set in unrelated rql when manipulating the AST