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) |