diff -r 3e51c2a577dd -r 64639bc94e25 entity.py --- a/entity.py Thu Oct 20 14:22:16 2011 +0200 +++ b/entity.py Thu Oct 20 16:03:51 2011 +0200 @@ -37,7 +37,8 @@ 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 soup2xhtml @@ -65,6 +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 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 + 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 @@ -931,17 +1011,22 @@ # 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 @@ -955,20 +1040,20 @@ searchedvar = subjvar = select.get_variable('S') evar = objvar = select.get_variable('O') select.add_selected(searchedvar) - # initialize some variables according to `self` existance + # 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 - var = select.get_variable('ZZ') # XXX unname when tests pass + variable = select.make_variable() if role == 'subject': - rel = make_relation(var, rtype.type, (searchedvar,), VariableRef) + rel = make_relation(variable, rtype, (searchedvar,), VariableRef) else: - rel = make_relation(searchedvar, rtype.type, (var,), VariableRef) + 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 - rel = make_relation(subjvar, rtype.type, (objvar,), VariableRef) + rel = make_relation(subjvar, rtype, (objvar,), VariableRef) select.add_restriction(Not(rel)) if self.has_eid(): rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant) @@ -998,11 +1083,23 @@ 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.name in cstr.mainvars: - if not self.has_eid() and evar.name in cstr.mainvars: - continue + 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) @@ -1028,12 +1125,13 @@ 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