entity.py
changeset 7973 64639bc94e25
parent 7964 4ea2abc83dce
child 7990 a673d1d9a738
equal deleted inserted replaced
7971:3e51c2a577dd 7973:64639bc94e25
    35 from cubicweb.utils import support_args
    35 from cubicweb.utils import support_args
    36 from cubicweb.rset import ResultSet
    36 from cubicweb.rset import ResultSet
    37 from cubicweb.selectors import yes
    37 from cubicweb.selectors import yes
    38 from cubicweb.appobject import AppObject
    38 from cubicweb.appobject import AppObject
    39 from cubicweb.req import _check_cw_unsafe
    39 from cubicweb.req import _check_cw_unsafe
    40 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
    40 from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
       
    41                              GeneratedConstraint)
    41 from cubicweb.rqlrewrite import RQLRewriter
    42 from cubicweb.rqlrewrite import RQLRewriter
    42 
    43 
    43 from cubicweb.uilib import soup2xhtml
    44 from cubicweb.uilib import soup2xhtml
    44 from cubicweb.mixins import MI_REL_TRIGGERS
    45 from cubicweb.mixins import MI_REL_TRIGGERS
    45 from cubicweb.mttransforms import ENGINE
    46 from cubicweb.mttransforms import ENGINE
    63     # behind Apache mod_proxy
    64     # behind Apache mod_proxy
    64     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
    65     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
    65         return False
    66         return False
    66     return True
    67     return True
    67 
    68 
       
    69 def rel_vars(rel):
       
    70     return ((isinstance(rel.children[0], VariableRef)
       
    71              and rel.children[0].variable or None),
       
    72             (isinstance(rel.children[1].children[0], VariableRef)
       
    73              and rel.children[1].children[0].variable or None)
       
    74             )
       
    75 
       
    76 def rel_matches(rel, rtype, role, varname, operator='='):
       
    77     if rel.r_type == rtype and rel.children[1].operator == operator:
       
    78         same_role_var_idx = 0 if role == 'subject' else 1
       
    79         variables = rel_vars(rel)
       
    80         if variables[same_role_var_idx].name == varname:
       
    81             return variables[1 - same_role_var_idx]
       
    82 
       
    83 def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
       
    84                                  lt_infos, eidvars):
       
    85     """restrict vocabulary as much as possible in entity creation,
       
    86     based on infos provided by __linkto form param.
       
    87 
       
    88     Example based on following schema:
       
    89 
       
    90       class works_in(RelationDefinition):
       
    91           subject = 'CWUser'
       
    92           object = 'Lab'
       
    93           cardinality = '1*'
       
    94           constraints = [RQLConstraint('S in_group G, O welcomes G')]
       
    95 
       
    96       class welcomes(RelationDefinition):
       
    97           subject = 'Lab'
       
    98           object = 'CWGroup'
       
    99 
       
   100     If you create a CWUser in the "scientists" CWGroup you can show
       
   101     only the labs that welcome them using :
       
   102 
       
   103       lt_infos = {('in_group', 'subject'): 321}
       
   104 
       
   105     You get following restriction : 'O welcomes G, G eid 321'
       
   106 
       
   107     """
       
   108     st = cstr.snippet_rqlst.copy()
       
   109     # replace relations in ST by eid infos from linkto where possible
       
   110     for (info_rtype, info_role), eids in lt_infos.iteritems():
       
   111         eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
       
   112         for rel in st.iget_nodes(RqlRelation):
       
   113             targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
       
   114             if targetvar is not None:
       
   115                 if targetvar.name in eidvars:
       
   116                     rel.parent.remove(rel)
       
   117                 else:
       
   118                     eidrel = make_relation(
       
   119                         targetvar, 'eid', (targetvar.name, 'Substitute'),
       
   120                         Constant)
       
   121                     rel.parent.replace(rel, eidrel)
       
   122                     args[targetvar.name] = eid
       
   123                     eidvars.add(targetvar.name)
       
   124     # if modified ST still contains evar references we must discard the
       
   125     # constraint, otherwise evar is unknown in the final rql query which can
       
   126     # lead to a SQL table cartesian product and multiple occurences of solutions
       
   127     evarname = evar.name
       
   128     for rel in st.iget_nodes(RqlRelation):
       
   129         for variable in rel_vars(rel):
       
   130             if variable and evarname == variable.name:
       
   131                 return
       
   132     # else insert snippets into the global tree
       
   133     return GeneratedConstraint(st, cstr.mainvars - set(evarname))
       
   134 
       
   135 def pruned_lt_info(eschema, lt_infos):
       
   136     pruned = {}
       
   137     for (lt_rtype, lt_role), eids in lt_infos.iteritems():
       
   138         # we can only use lt_infos describing relation with a cardinality
       
   139         # of value 1 towards the linked entity
       
   140         if not len(eids) == 1:
       
   141             continue
       
   142         lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
       
   143             0 if lt_role == 'subject' else 1]
       
   144         if lt_card not in '?1':
       
   145             continue
       
   146         pruned[(lt_rtype, lt_role)] = eids
       
   147     return pruned
    68 
   148 
    69 class Entity(AppObject):
   149 class Entity(AppObject):
    70     """an entity instance has e_schema automagically set on
   150     """an entity instance has e_schema automagically set on
    71     the class and instances has access to their issuing cursor.
   151     the class and instances has access to their issuing cursor.
    72 
   152 
   929         return select.as_string()
  1009         return select.as_string()
   930 
  1010 
   931     # generic vocabulary methods ##############################################
  1011     # generic vocabulary methods ##############################################
   932 
  1012 
   933     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
  1013     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   934                          vocabconstraints=True):
  1014                          vocabconstraints=True, lt_infos={}):
   935         """build a rql to fetch `targettype` entities unrelated to this entity
  1015         """build a rql to fetch `targettype` entities unrelated to this entity
   936         using (rtype, role) relation.
  1016         using (rtype, role) relation.
   937 
  1017 
   938         Consider relation permissions so that returned entities may be actually
  1018         Consider relation permissions so that returned entities may be actually
   939         linked by `rtype`.
  1019         linked by `rtype`.
       
  1020 
       
  1021         `lt_infos` are supplementary informations, usually coming from __linkto
       
  1022         parameter, that can help further restricting the results in case current
       
  1023         entity is not yet created. It is a dict describing entities the current
       
  1024         entity will be linked to, which keys are (rtype, role) tuples and values
       
  1025         are a list of eids.
   940         """
  1026         """
   941         ordermethod = ordermethod or 'fetch_unrelated_order'
  1027         ordermethod = ordermethod or 'fetch_unrelated_order'
   942         if isinstance(rtype, basestring):
  1028         rschema = self._cw.vreg.schema.rschema(rtype)
   943             rtype = self._cw.vreg.schema.rschema(rtype)
  1029         rdef = rschema.role_rdef(self.e_schema, targettype, role)
   944         rdef = rtype.role_rdef(self.e_schema, targettype, role)
       
   945         rewriter = RQLRewriter(self._cw)
  1030         rewriter = RQLRewriter(self._cw)
   946         select = Select()
  1031         select = Select()
   947         # initialize some variables according to the `role` of `self` in the
  1032         # initialize some variables according to the `role` of `self` in the
   948         # relation (variable names must respect constraints conventions):
  1033         # relation (variable names must respect constraints conventions):
   949         # * variable for myself (`evar`)
  1034         # * variable for myself (`evar`)
   953             searchedvar = objvar = select.get_variable('O')
  1038             searchedvar = objvar = select.get_variable('O')
   954         else:
  1039         else:
   955             searchedvar = subjvar = select.get_variable('S')
  1040             searchedvar = subjvar = select.get_variable('S')
   956             evar = objvar = select.get_variable('O')
  1041             evar = objvar = select.get_variable('O')
   957         select.add_selected(searchedvar)
  1042         select.add_selected(searchedvar)
   958         # initialize some variables according to `self` existance
  1043         # initialize some variables according to `self` existence
   959         if rdef.role_cardinality(neg_role(role)) in '?1':
  1044         if rdef.role_cardinality(neg_role(role)) in '?1':
   960             # if cardinality in '1?', we want a target entity which isn't
  1045             # if cardinality in '1?', we want a target entity which isn't
   961             # already linked using this relation
  1046             # already linked using this relation
   962             var = select.get_variable('ZZ') # XXX unname when tests pass
  1047             variable = select.make_variable()
   963             if role == 'subject':
  1048             if role == 'subject':
   964                 rel = make_relation(var, rtype.type, (searchedvar,), VariableRef)
  1049                 rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
   965             else:
  1050             else:
   966                 rel = make_relation(searchedvar, rtype.type, (var,), VariableRef)
  1051                 rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
   967             select.add_restriction(Not(rel))
  1052             select.add_restriction(Not(rel))
   968         elif self.has_eid():
  1053         elif self.has_eid():
   969             # elif we have an eid, we don't want a target entity which is
  1054             # elif we have an eid, we don't want a target entity which is
   970             # already linked to ourself through this relation
  1055             # already linked to ourself through this relation
   971             rel = make_relation(subjvar, rtype.type, (objvar,), VariableRef)
  1056             rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
   972             select.add_restriction(Not(rel))
  1057             select.add_restriction(Not(rel))
   973         if self.has_eid():
  1058         if self.has_eid():
   974             rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
  1059             rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
   975             select.add_restriction(rel)
  1060             select.add_restriction(rel)
   976             args = {'x': self.eid}
  1061             args = {'x': self.eid}
   996             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
  1081             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
   997             # will be included as well
  1082             # will be included as well
   998             cstrcls = RQLVocabularyConstraint
  1083             cstrcls = RQLVocabularyConstraint
   999         else:
  1084         else:
  1000             cstrcls = RQLConstraint
  1085             cstrcls = RQLConstraint
       
  1086         lt_infos = pruned_lt_info(self.e_schema, lt_infos or {})
       
  1087         # if there are still lt_infos, use set to keep track of added eid
       
  1088         # relations (adding twice the same eid relation is incorrect RQL)
       
  1089         eidvars = set()
  1001         for cstr in rdef.constraints:
  1090         for cstr in rdef.constraints:
  1002             # consider constraint.mainvars to check if constraint apply
  1091             # consider constraint.mainvars to check if constraint apply
  1003             if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
  1092             if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
  1004                 if not self.has_eid() and evar.name in cstr.mainvars:
  1093                 if not self.has_eid():
  1005                     continue
  1094                     if lt_infos:
       
  1095                         # we can perhaps further restrict with linkto infos using
       
  1096                         # a custom constraint built from cstr and lt_infos
       
  1097                         cstr = build_cstr_with_linkto_infos(
       
  1098                             cstr, args, searchedvar, evar, lt_infos, eidvars)
       
  1099                         if cstr is None:
       
  1100                             continue # could not build constraint -> discard
       
  1101                     elif evar.name in cstr.mainvars:
       
  1102                         continue
  1006                 # compute a varmap suitable to RQLRewriter.rewrite argument
  1103                 # compute a varmap suitable to RQLRewriter.rewrite argument
  1007                 varmap = dict((v, v) for v in (searchedvar.name, evar.name)
  1104                 varmap = dict((v, v) for v in (searchedvar.name, evar.name)
  1008                               if v in select.defined_vars and v in cstr.mainvars)
  1105                               if v in select.defined_vars and v in cstr.mainvars)
  1009                 # rewrite constraint by constraint since we want a AND between
  1106                 # rewrite constraint by constraint since we want a AND between
  1010                 # expressions.
  1107                 # expressions.
  1026         # we're done, turn the rql syntax tree as a string
  1123         # we're done, turn the rql syntax tree as a string
  1027         rql = select.as_string()
  1124         rql = select.as_string()
  1028         return rql, args
  1125         return rql, args
  1029 
  1126 
  1030     def unrelated(self, rtype, targettype, role='subject', limit=None,
  1127     def unrelated(self, rtype, targettype, role='subject', limit=None,
  1031                   ordermethod=None): # XXX .cw_unrelated
  1128                   ordermethod=None, lt_infos={}): # XXX .cw_unrelated
  1032         """return a result set of target type objects that may be related
  1129         """return a result set of target type objects that may be related
  1033         by a given relation, with self as subject or object
  1130         by a given relation, with self as subject or object
  1034         """
  1131         """
  1035         try:
  1132         try:
  1036             rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
  1133             rql, args = self.cw_unrelated_rql(rtype, targettype, role,
       
  1134                                               ordermethod, lt_infos=lt_infos)
  1037         except Unauthorized:
  1135         except Unauthorized:
  1038             return self._cw.empty_rset()
  1136             return self._cw.empty_rset()
  1039         # XXX should be set in unrelated rql when manipulating the AST
  1137         # XXX should be set in unrelated rql when manipulating the AST
  1040         if limit is not None:
  1138         if limit is not None:
  1041             before, after = rql.split(' WHERE ', 1)
  1139             before, after = rql.split(' WHERE ', 1)