59 # behind Apache mod_proxy |
64 # behind Apache mod_proxy |
60 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: |
61 return False |
66 return False |
62 return True |
67 return True |
63 |
68 |
64 |
69 def rel_vars(rel): |
65 def remove_ambiguous_rels(attr_set, subjtypes, schema): |
70 return ((isinstance(rel.children[0], VariableRef) |
66 '''remove from `attr_set` the relations of entity types `subjtypes` that have |
71 and rel.children[0].variable or None), |
67 different entity type sets as target''' |
72 (isinstance(rel.children[1].children[0], VariableRef) |
68 for attr in attr_set.copy(): |
73 and rel.children[1].children[0].variable or None) |
69 rschema = schema.rschema(attr) |
74 ) |
70 if rschema.final: |
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: |
71 continue |
141 continue |
72 ttypes = None |
142 lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[ |
73 for subjtype in subjtypes: |
143 0 if lt_role == 'subject' else 1] |
74 cur_ttypes = rschema.objects(subjtype) |
144 if lt_card not in '?1': |
75 if ttypes is None: |
145 continue |
76 ttypes = cur_ttypes |
146 pruned[(lt_rtype, lt_role)] = eids |
77 elif cur_ttypes != ttypes: |
147 return pruned |
78 attr_set.remove(attr) |
|
79 break |
|
80 |
|
81 |
148 |
82 class Entity(AppObject): |
149 class Entity(AppObject): |
83 """an entity instance has e_schema automagically set on |
150 """an entity instance has e_schema automagically set on |
84 the class and instances has access to their issuing cursor. |
151 the class and instances has access to their issuing cursor. |
85 |
152 |
151 mixins += cls.__bases__[1:] |
219 mixins += cls.__bases__[1:] |
152 cls.__bases__ = tuple(mixins) |
220 cls.__bases__ = tuple(mixins) |
153 cls.info('plugged %s mixins on %s', mixins, cls) |
221 cls.info('plugged %s mixins on %s', mixins, cls) |
154 |
222 |
155 fetch_attrs = ('modification_date',) |
223 fetch_attrs = ('modification_date',) |
|
224 |
156 @classmethod |
225 @classmethod |
157 def fetch_order(cls, attr, var): |
226 def cw_fetch_order(cls, select, attr, var): |
158 """class method used to control sort order when multiple entities of |
227 """This class method may be used to control sort order when multiple |
159 this type are fetched |
228 entities of this type are fetched through ORM methods. Its arguments |
160 """ |
229 are: |
161 return cls.fetch_unrelated_order(attr, var) |
230 |
|
231 * `select`, the RQL syntax tree |
|
232 |
|
233 * `attr`, the attribute being watched |
|
234 |
|
235 * `var`, the variable through which this attribute's value may be |
|
236 accessed in the query |
|
237 |
|
238 When you want to do some sorting on the given attribute, you should |
|
239 modify the syntax tree accordingly. For instance: |
|
240 |
|
241 .. sourcecode:: python |
|
242 |
|
243 from rql import nodes |
|
244 |
|
245 class Version(AnyEntity): |
|
246 __regid__ = 'Version' |
|
247 |
|
248 fetch_attrs = ('num', 'description', 'in_state') |
|
249 |
|
250 @classmethod |
|
251 def cw_fetch_order(cls, select, attr, var): |
|
252 if attr == 'num': |
|
253 func = nodes.Function('version_sort_value') |
|
254 func.append(nodes.variable_ref(var)) |
|
255 sterm = nodes.SortTerm(func, asc=False) |
|
256 select.add_sort_term(sterm) |
|
257 |
|
258 The default implementation call |
|
259 :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` |
|
260 """ |
|
261 cls.cw_fetch_unrelated_order(select, attr, var) |
162 |
262 |
163 @classmethod |
263 @classmethod |
164 def fetch_unrelated_order(cls, attr, var): |
264 def cw_fetch_unrelated_order(cls, select, attr, var): |
165 """class method used to control sort order when multiple entities of |
265 """This class method may be used to control sort order when multiple entities of |
166 this type are fetched to use in edition (eg propose them to create a |
266 this type are fetched to use in edition (e.g. propose them to create a |
167 new relation on an edited entity). |
267 new relation on an edited entity). |
|
268 |
|
269 See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a |
|
270 description of its arguments and usage. |
|
271 |
|
272 By default entities will be listed on their modification date descending, |
|
273 i.e. you'll get entities recently modified first. |
168 """ |
274 """ |
169 if attr == 'modification_date': |
275 if attr == 'modification_date': |
170 return '%s DESC' % var |
276 select.add_sort_var(var, asc=False) |
171 return None |
|
172 |
277 |
173 @classmethod |
278 @classmethod |
174 def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', |
279 def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', |
175 settype=True, ordermethod='fetch_order'): |
280 settype=True, ordermethod='fetch_order'): |
176 """return a rql to fetch all entities of the class type""" |
281 st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs, |
177 # XXX update api and implementation to AST manipulation (see unrelated rql) |
282 settype=settype, ordermethod=ordermethod) |
178 restrictions = restriction or [] |
283 rql = st.as_string() |
|
284 if restriction: |
|
285 # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction |
|
286 warn('[3.14] fetch_rql: use of `restriction` parameter is ' |
|
287 'deprecated, please use fetch_rqlst and supply a syntax' |
|
288 'tree with your restriction instead', DeprecationWarning) |
|
289 insert = ' WHERE ' + ','.join(restriction) |
|
290 if ' WHERE ' in rql: |
|
291 select, where = rql.split(' WHERE ', 1) |
|
292 rql = select + insert + ',' + where |
|
293 else: |
|
294 rql += insert |
|
295 return rql |
|
296 |
|
297 @classmethod |
|
298 def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None, |
|
299 settype=True, ordermethod='fetch_order'): |
|
300 if select is None: |
|
301 select = Select() |
|
302 mainvar = select.get_variable(mainvar) |
|
303 select.add_selected(mainvar) |
|
304 elif isinstance(mainvar, basestring): |
|
305 assert mainvar in select.defined_vars |
|
306 mainvar = select.get_variable(mainvar) |
|
307 # eases string -> syntax tree test transition: please remove once stable |
|
308 select._varmaker = rqlvar_maker(defined=select.defined_vars, |
|
309 aliases=select.aliases, index=26) |
179 if settype: |
310 if settype: |
180 restrictions.append('%s is %s' % (mainvar, cls.__regid__)) |
311 select.add_type_restriction(mainvar, cls.__regid__) |
181 if fetchattrs is None: |
312 if fetchattrs is None: |
182 fetchattrs = cls.fetch_attrs |
313 fetchattrs = cls.fetch_attrs |
183 selection = [mainvar] |
314 cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod) |
184 orderby = [] |
315 return select |
185 # start from 26 to avoid possible conflicts with X |
|
186 # XXX not enough to be sure it'll be no conflicts |
|
187 varmaker = rqlvar_maker(index=26) |
|
188 cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection, |
|
189 orderby, restrictions, user, ordermethod) |
|
190 rql = 'Any %s' % ','.join(selection) |
|
191 if orderby: |
|
192 rql += ' ORDERBY %s' % ','.join(orderby) |
|
193 rql += ' WHERE %s' % ', '.join(restrictions) |
|
194 return rql |
|
195 |
316 |
196 @classmethod |
317 @classmethod |
197 def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs, |
318 def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema): |
198 selection, orderby, restrictions, user, |
319 """find rtypes in `fetchattrs` that relate different subject etypes |
199 ordermethod='fetch_order', visited=None): |
320 taken from (`subjtypes`) to different target etypes; these so called |
|
321 "ambiguous" relations, are added directly to the `select` syntax tree |
|
322 selection but removed from `fetchattrs` to avoid the fetch recursion |
|
323 because we have to choose only one targettype for the recursion and |
|
324 adding its own fetch attrs to the selection -when we recurse- would |
|
325 filter out the other possible target types from the result set |
|
326 """ |
|
327 for attr in fetchattrs.copy(): |
|
328 rschema = schema.rschema(attr) |
|
329 if rschema.final: |
|
330 continue |
|
331 ttypes = None |
|
332 for subjtype in subjtypes: |
|
333 cur_ttypes = set(rschema.objects(subjtype)) |
|
334 if ttypes is None: |
|
335 ttypes = cur_ttypes |
|
336 elif cur_ttypes != ttypes: |
|
337 # we found an ambiguous relation: remove it from fetchattrs |
|
338 fetchattrs.remove(attr) |
|
339 # ... and add it to the selection |
|
340 targetvar = select.make_variable() |
|
341 select.add_selected(targetvar) |
|
342 rel = make_relation(var, attr, (targetvar,), VariableRef) |
|
343 select.add_restriction(rel) |
|
344 break |
|
345 |
|
346 @classmethod |
|
347 def _fetch_restrictions(cls, mainvar, select, fetchattrs, |
|
348 user, ordermethod='fetch_order', visited=None): |
200 eschema = cls.e_schema |
349 eschema = cls.e_schema |
201 if visited is None: |
350 if visited is None: |
202 visited = set((eschema.type,)) |
351 visited = set((eschema.type,)) |
203 elif eschema.type in visited: |
352 elif eschema.type in visited: |
204 # avoid infinite recursion |
353 # avoid infinite recursion |
214 attr, cls.__regid__) |
363 attr, cls.__regid__) |
215 continue |
364 continue |
216 rdef = eschema.rdef(attr) |
365 rdef = eschema.rdef(attr) |
217 if not user.matching_groups(rdef.get_groups('read')): |
366 if not user.matching_groups(rdef.get_groups('read')): |
218 continue |
367 continue |
219 var = varmaker.next() |
368 if rschema.final or rdef.cardinality[0] in '?1': |
220 selection.append(var) |
369 var = select.make_variable() |
221 restriction = '%s %s %s' % (mainvar, attr, var) |
370 select.add_selected(var) |
222 restrictions.append(restriction) |
371 rel = make_relation(mainvar, attr, (var,), VariableRef) |
|
372 select.add_restriction(rel) |
|
373 else: |
|
374 cls.warning('bad relation %s specified in fetch attrs for %s', |
|
375 attr, cls) |
|
376 continue |
223 if not rschema.final: |
377 if not rschema.final: |
224 card = rdef.cardinality[0] |
|
225 if card not in '?1': |
|
226 cls.warning('bad relation %s specified in fetch attrs for %s', |
|
227 attr, cls) |
|
228 selection.pop() |
|
229 restrictions.pop() |
|
230 continue |
|
231 # XXX we need outer join in case the relation is not mandatory |
378 # XXX we need outer join in case the relation is not mandatory |
232 # (card == '?') *or if the entity is being added*, since in |
379 # (card == '?') *or if the entity is being added*, since in |
233 # that case the relation may still be missing. As we miss this |
380 # that case the relation may still be missing. As we miss this |
234 # later information here, systematically add it. |
381 # later information here, systematically add it. |
235 restrictions[-1] += '?' |
382 rel.change_optional('right') |
236 targettypes = rschema.objects(eschema.type) |
383 targettypes = rschema.objects(eschema.type) |
237 # XXX user._cw.vreg iiiirk |
384 vreg = user._cw.vreg # XXX user._cw.vreg iiiirk |
238 etypecls = user._cw.vreg['etypes'].etype_class(targettypes[0]) |
385 etypecls = vreg['etypes'].etype_class(targettypes[0]) |
239 if len(targettypes) > 1: |
386 if len(targettypes) > 1: |
240 # find fetch_attrs common to all destination types |
387 # find fetch_attrs common to all destination types |
241 fetchattrs = user._cw.vreg['etypes'].fetch_attrs(targettypes) |
388 fetchattrs = vreg['etypes'].fetch_attrs(targettypes) |
242 remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema) |
389 # ... and handle ambiguous relations |
|
390 cls._fetch_ambiguous_rtypes(select, var, fetchattrs, |
|
391 targettypes, vreg.schema) |
243 else: |
392 else: |
244 fetchattrs = etypecls.fetch_attrs |
393 fetchattrs = etypecls.fetch_attrs |
245 etypecls._fetch_restrictions(var, varmaker, fetchattrs, |
394 etypecls._fetch_restrictions(var, select, fetchattrs, |
246 selection, orderby, restrictions, |
|
247 user, ordermethod, visited=visited) |
395 user, ordermethod, visited=visited) |
248 if ordermethod is not None: |
396 if ordermethod is not None: |
249 orderterm = getattr(cls, ordermethod)(attr, var) |
397 try: |
250 if orderterm: |
398 cmeth = getattr(cls, ordermethod) |
251 orderby.append(orderterm) |
399 warn('[3.14] %s %s class method should be renamed to cw_%s' |
252 return selection, orderby, restrictions |
400 % (cls.__regid__, ordermethod, ordermethod), |
|
401 DeprecationWarning) |
|
402 except AttributeError: |
|
403 cmeth = getattr(cls, 'cw_' + ordermethod) |
|
404 if support_args(cmeth, 'select'): |
|
405 cmeth(select, attr, var) |
|
406 else: |
|
407 warn('[3.14] %s should now take (select, attr, var) and ' |
|
408 'modify the syntax tree when desired instead of ' |
|
409 'returning something' % cmeth, DeprecationWarning) |
|
410 orderterm = cmeth(attr, var.name) |
|
411 if orderterm is not None: |
|
412 try: |
|
413 var, order = orderterm.split() |
|
414 except ValueError: |
|
415 if '(' in orderterm: |
|
416 cls.error('ignore %s until %s is upgraded', |
|
417 orderterm, cmeth) |
|
418 orderterm = None |
|
419 elif not ' ' in orderterm.strip(): |
|
420 var = orderterm |
|
421 order = 'ASC' |
|
422 if orderterm is not None: |
|
423 select.add_sort_var(select.get_variable(var), |
|
424 order=='ASC') |
253 |
425 |
254 @classmethod |
426 @classmethod |
255 @cached |
427 @cached |
256 def _rest_attr_info(cls): |
428 def cw_rest_attr_info(cls): |
|
429 """this class method return an attribute name to be used in URL for |
|
430 entities of this type and a boolean flag telling if its value should be |
|
431 checked for uniqness. |
|
432 |
|
433 The attribute returned is, in order of priority: |
|
434 |
|
435 * class's `rest_attr` class attribute |
|
436 * an attribute defined as unique in the class'schema |
|
437 * 'eid' |
|
438 """ |
257 mainattr, needcheck = 'eid', True |
439 mainattr, needcheck = 'eid', True |
258 if cls.rest_attr: |
440 if cls.rest_attr: |
259 mainattr = cls.rest_attr |
441 mainattr = cls.rest_attr |
260 needcheck = not cls.e_schema.has_unique_values(mainattr) |
442 needcheck = not cls.e_schema.has_unique_values(mainattr) |
261 else: |
443 else: |
262 for rschema in cls.e_schema.subject_relations(): |
444 for rschema in cls.e_schema.subject_relations(): |
263 if rschema.final and rschema != 'eid' and cls.e_schema.has_unique_values(rschema): |
445 if rschema.final and rschema != 'eid' \ |
|
446 and cls.e_schema.has_unique_values(rschema): |
264 mainattr = str(rschema) |
447 mainattr = str(rschema) |
265 needcheck = False |
448 needcheck = False |
266 break |
449 break |
267 if mainattr == 'eid': |
450 if mainattr == 'eid': |
268 needcheck = False |
451 needcheck = False |
755 rset = self._cw.empty_rset() |
945 rset = self._cw.empty_rset() |
756 self.cw_set_relation_cache(rtype, role, rset) |
946 self.cw_set_relation_cache(rtype, role, rset) |
757 return self.related(rtype, role, limit, entities) |
947 return self.related(rtype, role, limit, entities) |
758 |
948 |
759 def cw_related_rql(self, rtype, role='subject', targettypes=None): |
949 def cw_related_rql(self, rtype, role='subject', targettypes=None): |
760 rschema = self._cw.vreg.schema[rtype] |
950 vreg = self._cw.vreg |
|
951 rschema = vreg.schema[rtype] |
|
952 select = Select() |
|
953 mainvar, evar = select.get_variable('X'), select.get_variable('E') |
|
954 select.add_selected(mainvar) |
|
955 select.add_eid_restriction(evar, 'x', 'Substitute') |
761 if role == 'subject': |
956 if role == 'subject': |
762 restriction = 'E eid %%(x)s, E %s X' % rtype |
957 rel = make_relation(evar, rtype, (mainvar,), VariableRef) |
|
958 select.add_restriction(rel) |
763 if targettypes is None: |
959 if targettypes is None: |
764 targettypes = rschema.objects(self.e_schema) |
960 targettypes = rschema.objects(self.e_schema) |
765 else: |
961 else: |
766 restriction += ', X is IN (%s)' % ','.join(targettypes) |
962 select.add_constant_restriction(mainvar, 'is', |
767 card = greater_card(rschema, (self.e_schema,), targettypes, 0) |
963 targettypes, 'etype') |
|
964 gcard = greater_card(rschema, (self.e_schema,), targettypes, 0) |
768 else: |
965 else: |
769 restriction = 'E eid %%(x)s, X %s E' % rtype |
966 rel = make_relation(mainvar, rtype, (evar,), VariableRef) |
|
967 select.add_restriction(rel) |
770 if targettypes is None: |
968 if targettypes is None: |
771 targettypes = rschema.subjects(self.e_schema) |
969 targettypes = rschema.subjects(self.e_schema) |
772 else: |
970 else: |
773 restriction += ', X is IN (%s)' % ','.join(targettypes) |
971 select.add_constant_restriction(mainvar, 'is', targettypes, |
774 card = greater_card(rschema, targettypes, (self.e_schema,), 1) |
972 'etype') |
775 etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0]) |
973 gcard = greater_card(rschema, targettypes, (self.e_schema,), 1) |
|
974 etypecls = vreg['etypes'].etype_class(targettypes[0]) |
776 if len(targettypes) > 1: |
975 if len(targettypes) > 1: |
777 fetchattrs = self._cw.vreg['etypes'].fetch_attrs(targettypes) |
976 fetchattrs = vreg['etypes'].fetch_attrs(targettypes) |
778 # XXX we should fetch ambiguous relation objects too but not |
977 self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs, |
779 # recurse on them in _fetch_restrictions; it is easier to remove |
978 targettypes, vreg.schema) |
780 # them completely for now, as it would require an deeper api rewrite |
|
781 remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema) |
|
782 else: |
979 else: |
783 fetchattrs = etypecls.fetch_attrs |
980 fetchattrs = etypecls.fetch_attrs |
784 rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs, |
981 etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs, |
785 settype=False) |
982 settype=False) |
786 # optimisation: remove ORDERBY if cardinality is 1 or ? (though |
983 # optimisation: remove ORDERBY if cardinality is 1 or ? (though |
787 # greater_card return 1 for those both cases) |
984 # greater_card return 1 for those both cases) |
788 if card == '1': |
985 if gcard == '1': |
789 if ' ORDERBY ' in rql: |
986 select.remove_sort_terms() |
790 rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0], |
987 elif not select.orderby: |
791 rql.split(' WHERE ', 1)[1]) |
988 # if modification_date is already retrieved, we use it instead |
792 elif not ' ORDERBY ' in rql: |
989 # of adding another variable for sorting. This should not be |
793 args = rql.split(' WHERE ', 1) |
990 # problematic, but it is with sqlserver, see ticket #694445 |
794 # if modification_date already retrieved, we should use it instead |
991 for rel in select.where.get_nodes(RqlRelation): |
795 # of adding another variable for sort. This should be be problematic |
992 if (rel.r_type == 'modification_date' |
796 # but it's actually with sqlserver, see ticket #694445 |
993 and rel.children[0].variable == mainvar |
797 if 'X modification_date ' in args[1]: |
994 and rel.children[1].operator == '='): |
798 var = args[1].split('X modification_date ', 1)[1].split(',', 1)[0] |
995 var = rel.children[1].children[0].variable |
799 args.insert(1, var.strip()) |
996 select.add_sort_var(var, asc=False) |
800 rql = '%s ORDERBY %s DESC WHERE %s' % tuple(args) |
997 break |
801 else: |
998 else: |
802 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % \ |
999 mdvar = select.make_variable() |
803 tuple(args) |
1000 rel = make_relation(mainvar, 'modification_date', |
804 return rql |
1001 (mdvar,), VariableRef) |
|
1002 select.add_restriction(rel) |
|
1003 select.add_sort_var(mdvar, asc=False) |
|
1004 return select.as_string() |
805 |
1005 |
806 # generic vocabulary methods ############################################## |
1006 # generic vocabulary methods ############################################## |
807 |
1007 |
808 def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None, |
1008 def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None, |
809 vocabconstraints=True): |
1009 vocabconstraints=True, lt_infos={}): |
810 """build a rql to fetch `targettype` entities unrelated to this entity |
1010 """build a rql to fetch `targettype` entities unrelated to this entity |
811 using (rtype, role) relation. |
1011 using (rtype, role) relation. |
812 |
1012 |
813 Consider relation permissions so that returned entities may be actually |
1013 Consider relation permissions so that returned entities may be actually |
814 linked by `rtype`. |
1014 linked by `rtype`. |
|
1015 |
|
1016 `lt_infos` are supplementary informations, usually coming from __linkto |
|
1017 parameter, that can help further restricting the results in case current |
|
1018 entity is not yet created. It is a dict describing entities the current |
|
1019 entity will be linked to, which keys are (rtype, role) tuples and values |
|
1020 are a list of eids. |
815 """ |
1021 """ |
816 ordermethod = ordermethod or 'fetch_unrelated_order' |
1022 ordermethod = ordermethod or 'fetch_unrelated_order' |
817 if isinstance(rtype, basestring): |
1023 rschema = self._cw.vreg.schema.rschema(rtype) |
818 rtype = self._cw.vreg.schema.rschema(rtype) |
1024 rdef = rschema.role_rdef(self.e_schema, targettype, role) |
819 rdef = rtype.role_rdef(self.e_schema, targettype, role) |
|
820 rewriter = RQLRewriter(self._cw) |
1025 rewriter = RQLRewriter(self._cw) |
|
1026 select = Select() |
821 # initialize some variables according to the `role` of `self` in the |
1027 # initialize some variables according to the `role` of `self` in the |
822 # relation: |
1028 # relation (variable names must respect constraints conventions): |
823 # * variable for myself (`evar`) and searched entities (`searchvedvar`) |
1029 # * variable for myself (`evar`) |
824 # * entity type of the subject (`subjtype`) and of the object |
1030 # * variable for searched entities (`searchvedvar`) |
825 # (`objtype`) of the relation |
|
826 if role == 'subject': |
1031 if role == 'subject': |
827 evar, searchedvar = 'S', 'O' |
1032 evar = subjvar = select.get_variable('S') |
828 subjtype, objtype = self.e_schema, targettype |
1033 searchedvar = objvar = select.get_variable('O') |
829 else: |
1034 else: |
830 searchedvar, evar = 'S', 'O' |
1035 searchedvar = subjvar = select.get_variable('S') |
831 objtype, subjtype = self.e_schema, targettype |
1036 evar = objvar = select.get_variable('O') |
832 # initialize some variables according to `self` existance |
1037 select.add_selected(searchedvar) |
|
1038 # initialize some variables according to `self` existence |
833 if rdef.role_cardinality(neg_role(role)) in '?1': |
1039 if rdef.role_cardinality(neg_role(role)) in '?1': |
834 # if cardinality in '1?', we want a target entity which isn't |
1040 # if cardinality in '1?', we want a target entity which isn't |
835 # already linked using this relation |
1041 # already linked using this relation |
836 if searchedvar == 'S': |
1042 variable = select.make_variable() |
837 restriction = ['NOT S %s ZZ' % rtype] |
1043 if role == 'subject': |
838 else: |
1044 rel = make_relation(variable, rtype, (searchedvar,), VariableRef) |
839 restriction = ['NOT ZZ %s O' % rtype] |
1045 else: |
|
1046 rel = make_relation(searchedvar, rtype, (variable,), VariableRef) |
|
1047 select.add_restriction(Not(rel)) |
840 elif self.has_eid(): |
1048 elif self.has_eid(): |
841 # elif we have an eid, we don't want a target entity which is |
1049 # elif we have an eid, we don't want a target entity which is |
842 # already linked to ourself through this relation |
1050 # already linked to ourself through this relation |
843 restriction = ['NOT S %s O' % rtype] |
1051 rel = make_relation(subjvar, rtype, (objvar,), VariableRef) |
844 else: |
1052 select.add_restriction(Not(rel)) |
845 restriction = [] |
|
846 if self.has_eid(): |
1053 if self.has_eid(): |
847 restriction += ['%s eid %%(x)s' % evar] |
1054 rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant) |
|
1055 select.add_restriction(rel) |
848 args = {'x': self.eid} |
1056 args = {'x': self.eid} |
849 if role == 'subject': |
1057 if role == 'subject': |
850 sec_check_args = {'fromeid': self.eid} |
1058 sec_check_args = {'fromeid': self.eid} |
851 else: |
1059 else: |
852 sec_check_args = {'toeid': self.eid} |
1060 sec_check_args = {'toeid': self.eid} |
853 existant = None # instead of 'SO', improve perfs |
1061 existant = None # instead of 'SO', improve perfs |
854 else: |
1062 else: |
855 args = {} |
1063 args = {} |
856 sec_check_args = {} |
1064 sec_check_args = {} |
857 existant = searchedvar |
1065 existant = searchedvar.name |
858 # retreive entity class for targettype to compute base rql |
1066 # undefine unused evar, or the type resolver will consider it |
|
1067 select.undefine_variable(evar) |
|
1068 # retrieve entity class for targettype to compute base rql |
859 etypecls = self._cw.vreg['etypes'].etype_class(targettype) |
1069 etypecls = self._cw.vreg['etypes'].etype_class(targettype) |
860 rql = etypecls.fetch_rql(self._cw.user, restriction, |
1070 etypecls.fetch_rqlst(self._cw.user, select, searchedvar, |
861 mainvar=searchedvar, ordermethod=ordermethod) |
1071 ordermethod=ordermethod) |
862 select = self._cw.vreg.parse(self._cw, rql, args).children[0] |
1072 # from now on, we need variable type resolving |
|
1073 self._cw.vreg.solutions(self._cw, select, args) |
863 # insert RQL expressions for schema constraints into the rql syntax tree |
1074 # insert RQL expressions for schema constraints into the rql syntax tree |
864 if vocabconstraints: |
1075 if vocabconstraints: |
865 # RQLConstraint is a subclass for RQLVocabularyConstraint, so they |
1076 # RQLConstraint is a subclass for RQLVocabularyConstraint, so they |
866 # will be included as well |
1077 # will be included as well |
867 cstrcls = RQLVocabularyConstraint |
1078 cstrcls = RQLVocabularyConstraint |
868 else: |
1079 else: |
869 cstrcls = RQLConstraint |
1080 cstrcls = RQLConstraint |
|
1081 lt_infos = pruned_lt_info(self.e_schema, lt_infos or {}) |
|
1082 # if there are still lt_infos, use set to keep track of added eid |
|
1083 # relations (adding twice the same eid relation is incorrect RQL) |
|
1084 eidvars = set() |
870 for cstr in rdef.constraints: |
1085 for cstr in rdef.constraints: |
871 # consider constraint.mainvars to check if constraint apply |
1086 # consider constraint.mainvars to check if constraint apply |
872 if isinstance(cstr, cstrcls) and searchedvar in cstr.mainvars: |
1087 if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars: |
873 if not self.has_eid() and evar in cstr.mainvars: |
1088 if not self.has_eid(): |
874 continue |
1089 if lt_infos: |
|
1090 # we can perhaps further restrict with linkto infos using |
|
1091 # a custom constraint built from cstr and lt_infos |
|
1092 cstr = build_cstr_with_linkto_infos( |
|
1093 cstr, args, searchedvar, evar, lt_infos, eidvars) |
|
1094 if cstr is None: |
|
1095 continue # could not build constraint -> discard |
|
1096 elif evar.name in cstr.mainvars: |
|
1097 continue |
875 # compute a varmap suitable to RQLRewriter.rewrite argument |
1098 # compute a varmap suitable to RQLRewriter.rewrite argument |
876 varmap = dict((v, v) for v in 'SO' if v in select.defined_vars |
1099 varmap = dict((v, v) for v in (searchedvar.name, evar.name) |
877 and v in cstr.mainvars) |
1100 if v in select.defined_vars and v in cstr.mainvars) |
878 # rewrite constraint by constraint since we want a AND between |
1101 # rewrite constraint by constraint since we want a AND between |
879 # expressions. |
1102 # expressions. |
880 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions, |
1103 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions, |
881 args, existant) |
1104 args, existant) |
882 # insert security RQL expressions granting the permission to 'add' the |
1105 # insert security RQL expressions granting the permission to 'add' the |
883 # relation into the rql syntax tree, if necessary |
1106 # relation into the rql syntax tree, if necessary |
884 rqlexprs = rdef.get_rqlexprs('add') |
1107 rqlexprs = rdef.get_rqlexprs('add') |
885 if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args): |
1108 if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args): |
886 # compute a varmap suitable to RQLRewriter.rewrite argument |
1109 # compute a varmap suitable to RQLRewriter.rewrite argument |
887 varmap = dict((v, v) for v in 'SO' if v in select.defined_vars) |
1110 varmap = dict((v, v) for v in (searchedvar.name, evar.name) |
|
1111 if v in select.defined_vars) |
888 # rewrite all expressions at once since we want a OR between them. |
1112 # rewrite all expressions at once since we want a OR between them. |
889 rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions, |
1113 rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions, |
890 args, existant) |
1114 args, existant) |
891 # ensure we have an order defined |
1115 # ensure we have an order defined |
892 if not select.orderby: |
1116 if not select.orderby: |
893 select.add_sort_var(select.defined_vars[searchedvar]) |
1117 select.add_sort_var(select.defined_vars[searchedvar.name]) |
894 # we're done, turn the rql syntax tree as a string |
1118 # we're done, turn the rql syntax tree as a string |
895 rql = select.as_string() |
1119 rql = select.as_string() |
896 return rql, args |
1120 return rql, args |
897 |
1121 |
898 def unrelated(self, rtype, targettype, role='subject', limit=None, |
1122 def unrelated(self, rtype, targettype, role='subject', limit=None, |
899 ordermethod=None): # XXX .cw_unrelated |
1123 ordermethod=None, lt_infos={}): # XXX .cw_unrelated |
900 """return a result set of target type objects that may be related |
1124 """return a result set of target type objects that may be related |
901 by a given relation, with self as subject or object |
1125 by a given relation, with self as subject or object |
902 """ |
1126 """ |
903 try: |
1127 try: |
904 rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod) |
1128 rql, args = self.cw_unrelated_rql(rtype, targettype, role, |
|
1129 ordermethod, lt_infos=lt_infos) |
905 except Unauthorized: |
1130 except Unauthorized: |
906 return self._cw.empty_rset() |
1131 return self._cw.empty_rset() |
907 # XXX should be set in unrelated rql when manipulating the AST |
1132 # XXX should be set in unrelated rql when manipulating the AST |
908 if limit is not None: |
1133 if limit is not None: |
909 before, after = rql.split(' WHERE ', 1) |
1134 before, after = rql.split(' WHERE ', 1) |