|
1 """Base class for entity objects manipulated in clients |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 """ |
|
7 __docformat__ = "restructuredtext en" |
|
8 |
|
9 from logilab.common import interface |
|
10 from logilab.common.compat import all |
|
11 from logilab.common.decorators import cached |
|
12 from logilab.mtconverter import TransformData, TransformError |
|
13 |
|
14 from rql.utils import rqlvar_maker |
|
15 |
|
16 from cubicweb import Unauthorized |
|
17 from cubicweb.vregistry import autoselectors |
|
18 from cubicweb.rset import ResultSet |
|
19 from cubicweb.selectors import yes |
|
20 from cubicweb.common.appobject import AppRsetObject |
|
21 from cubicweb.common.registerers import id_registerer |
|
22 from cubicweb.common.uilib import printable_value, html_escape, soup2xhtml |
|
23 from cubicweb.common.mixins import MI_REL_TRIGGERS |
|
24 from cubicweb.common.mttransforms import ENGINE |
|
25 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype |
|
26 |
|
27 _marker = object() |
|
28 |
|
29 def greater_card(rschema, subjtypes, objtypes, index): |
|
30 for subjtype in subjtypes: |
|
31 for objtype in objtypes: |
|
32 card = rschema.rproperty(subjtype, objtype, 'cardinality')[index] |
|
33 if card in '+*': |
|
34 return card |
|
35 return '1' |
|
36 |
|
37 |
|
38 class RelationTags(object): |
|
39 |
|
40 MODE_TAGS = frozenset(('link', 'create')) |
|
41 CATEGORY_TAGS = frozenset(('primary', 'secondary', 'generic', 'generated', |
|
42 'inlineview')) |
|
43 |
|
44 def __init__(self, eclass, tagdefs): |
|
45 self.eclass = eclass |
|
46 self._tagdefs = {} |
|
47 for relation, tags in tagdefs.iteritems(): |
|
48 # tags must become a set |
|
49 if isinstance(tags, basestring): |
|
50 tags = set((tags,)) |
|
51 elif not isinstance(tags, set): |
|
52 tags = set(tags) |
|
53 # relation must become a 3-uple (rtype, targettype, role) |
|
54 if isinstance(relation, basestring): |
|
55 self._tagdefs[(relation, '*', 'subject')] = tags |
|
56 self._tagdefs[(relation, '*', 'object')] = tags |
|
57 elif len(relation) == 1: # useful ? |
|
58 self._tagdefs[(relation[0], '*', 'subject')] = tags |
|
59 self._tagdefs[(relation[0], '*', 'object')] = tags |
|
60 elif len(relation) == 2: |
|
61 rtype, ttype = relation |
|
62 ttype = bw_normalize_etype(ttype) # XXX bw compat |
|
63 self._tagdefs[rtype, ttype, 'subject'] = tags |
|
64 self._tagdefs[rtype, ttype, 'object'] = tags |
|
65 elif len(relation) == 3: |
|
66 relation = list(relation) # XXX bw compat |
|
67 relation[1] = bw_normalize_etype(relation[1]) |
|
68 self._tagdefs[tuple(relation)] = tags |
|
69 else: |
|
70 raise ValueError('bad rtag definition (%r)' % (relation,)) |
|
71 |
|
72 |
|
73 def __initialize__(self): |
|
74 # eclass.[*]schema are only set when registering |
|
75 self.schema = self.eclass.schema |
|
76 eschema = self.eschema = self.eclass.e_schema |
|
77 rtags = self._tagdefs |
|
78 # expand wildcards in rtags and add automatic tags |
|
79 for rschema, tschemas, role in sorted(eschema.relation_definitions(True)): |
|
80 rtype = rschema.type |
|
81 star_tags = rtags.pop((rtype, '*', role), set()) |
|
82 for tschema in tschemas: |
|
83 tags = rtags.setdefault((rtype, tschema.type, role), set(star_tags)) |
|
84 if role == 'subject': |
|
85 X, Y = eschema, tschema |
|
86 card = rschema.rproperty(X, Y, 'cardinality')[0] |
|
87 composed = rschema.rproperty(X, Y, 'composite') == 'object' |
|
88 else: |
|
89 X, Y = tschema, eschema |
|
90 card = rschema.rproperty(X, Y, 'cardinality')[1] |
|
91 composed = rschema.rproperty(X, Y, 'composite') == 'subject' |
|
92 # set default category tags if needed |
|
93 if not tags & self.CATEGORY_TAGS: |
|
94 if card in '1+': |
|
95 if not rschema.is_final() and composed: |
|
96 category = 'generated' |
|
97 elif rschema.is_final() and ( |
|
98 rschema.type.endswith('_format') |
|
99 or rschema.type.endswith('_encoding')): |
|
100 category = 'generated' |
|
101 else: |
|
102 category = 'primary' |
|
103 elif rschema.is_final(): |
|
104 if (rschema.type.endswith('_format') |
|
105 or rschema.type.endswith('_encoding')): |
|
106 category = 'generated' |
|
107 else: |
|
108 category = 'secondary' |
|
109 else: |
|
110 category = 'generic' |
|
111 tags.add(category) |
|
112 if not tags & self.MODE_TAGS: |
|
113 if card in '?1': |
|
114 # by default, suppose link mode if cardinality doesn't allow |
|
115 # more than one relation |
|
116 mode = 'link' |
|
117 elif rschema.rproperty(X, Y, 'composite') == role: |
|
118 # if self is composed of the target type, create mode |
|
119 mode = 'create' |
|
120 else: |
|
121 # link mode by default |
|
122 mode = 'link' |
|
123 tags.add(mode) |
|
124 |
|
125 def _default_target(self, rschema, role='subject'): |
|
126 eschema = self.eschema |
|
127 if role == 'subject': |
|
128 return eschema.subject_relation(rschema).objects(eschema)[0] |
|
129 else: |
|
130 return eschema.object_relation(rschema).subjects(eschema)[0] |
|
131 |
|
132 # dict compat |
|
133 def __getitem__(self, key): |
|
134 if isinstance(key, basestring): |
|
135 key = (key,) |
|
136 return self.get_tags(*key) |
|
137 |
|
138 __contains__ = __getitem__ |
|
139 |
|
140 def get_tags(self, rtype, targettype=None, role='subject'): |
|
141 rschema = self.schema.rschema(rtype) |
|
142 if targettype is None: |
|
143 tschema = self._default_target(rschema, role) |
|
144 else: |
|
145 tschema = self.schema.eschema(targettype) |
|
146 return self._tagdefs[(rtype, tschema.type, role)] |
|
147 |
|
148 __call__ = get_tags |
|
149 |
|
150 def get_mode(self, rtype, targettype=None, role='subject'): |
|
151 # XXX: should we make an assertion on rtype not being final ? |
|
152 # assert not rschema.is_final() |
|
153 tags = self.get_tags(rtype, targettype, role) |
|
154 # do not change the intersection order ! |
|
155 modes = tags & self.MODE_TAGS |
|
156 assert len(modes) == 1 |
|
157 return modes.pop() |
|
158 |
|
159 def get_category(self, rtype, targettype=None, role='subject'): |
|
160 tags = self.get_tags(rtype, targettype, role) |
|
161 categories = tags & self.CATEGORY_TAGS |
|
162 assert len(categories) == 1 |
|
163 return categories.pop() |
|
164 |
|
165 def is_inlined(self, rtype, targettype=None, role='subject'): |
|
166 # return set(('primary', 'secondary')) & self.get_tags(rtype, targettype) |
|
167 return 'inlineview' in self.get_tags(rtype, targettype, role) |
|
168 |
|
169 |
|
170 class metaentity(autoselectors): |
|
171 """this metaclass sets the relation tags on the entity class |
|
172 and deals with the `widgets` attribute |
|
173 """ |
|
174 def __new__(mcs, name, bases, classdict): |
|
175 # collect baseclass' rtags |
|
176 tagdefs = {} |
|
177 widgets = {} |
|
178 for base in bases: |
|
179 tagdefs.update(getattr(base, '__rtags__', {})) |
|
180 widgets.update(getattr(base, 'widgets', {})) |
|
181 # update with the class' own rtgas |
|
182 tagdefs.update(classdict.get('__rtags__', {})) |
|
183 widgets.update(classdict.get('widgets', {})) |
|
184 # XXX decide whether or not it's a good idea to replace __rtags__ |
|
185 # good point: transparent support for inheritance levels >= 2 |
|
186 # bad point: we loose the information of which tags are specific |
|
187 # to this entity class |
|
188 classdict['__rtags__'] = tagdefs |
|
189 classdict['widgets'] = widgets |
|
190 eclass = super(metaentity, mcs).__new__(mcs, name, bases, classdict) |
|
191 # adds the "rtags" attribute |
|
192 eclass.rtags = RelationTags(eclass, tagdefs) |
|
193 return eclass |
|
194 |
|
195 |
|
196 class Entity(AppRsetObject, dict): |
|
197 """an entity instance has e_schema automagically set on |
|
198 the class and instances has access to their issuing cursor. |
|
199 |
|
200 A property is set for each attribute and relation on each entity's type |
|
201 class. Becare that among attributes, 'eid' is *NEITHER* stored in the |
|
202 dict containment (which acts as a cache for other attributes dynamically |
|
203 fetched) |
|
204 |
|
205 :type e_schema: `cubicweb.schema.EntitySchema` |
|
206 :ivar e_schema: the entity's schema |
|
207 |
|
208 :type rest_var: str |
|
209 :cvar rest_var: indicates which attribute should be used to build REST urls |
|
210 If None is specified, the first non-meta attribute will |
|
211 be used |
|
212 |
|
213 :type skip_copy_for: list |
|
214 :cvar skip_copy_for: a list of relations that should be skipped when copying |
|
215 this kind of entity. Note that some relations such |
|
216 as composite relations or relations that have '?1' as object |
|
217 cardinality |
|
218 """ |
|
219 __metaclass__ = metaentity |
|
220 __registry__ = 'etypes' |
|
221 __registerer__ = id_registerer |
|
222 __selectors__ = (yes,) |
|
223 widgets = {} |
|
224 id = None |
|
225 e_schema = None |
|
226 eid = None |
|
227 rest_attr = None |
|
228 skip_copy_for = () |
|
229 |
|
230 @classmethod |
|
231 def registered(cls, registry): |
|
232 """build class using descriptor at registration time""" |
|
233 assert cls.id is not None |
|
234 super(Entity, cls).registered(registry) |
|
235 if cls.id != 'Any': |
|
236 cls.__initialize__() |
|
237 return cls |
|
238 |
|
239 MODE_TAGS = set(('link', 'create')) |
|
240 CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata')) |
|
241 @classmethod |
|
242 def __initialize__(cls): |
|
243 """initialize a specific entity class by adding descriptors to access |
|
244 entity type's attributes and relations |
|
245 """ |
|
246 etype = cls.id |
|
247 assert etype != 'Any', etype |
|
248 cls.e_schema = eschema = cls.schema.eschema(etype) |
|
249 for rschema, _ in eschema.attribute_definitions(): |
|
250 if rschema.type == 'eid': |
|
251 continue |
|
252 setattr(cls, rschema.type, Attribute(rschema.type)) |
|
253 mixins = [] |
|
254 for rschema, _, x in eschema.relation_definitions(): |
|
255 if (rschema, x) in MI_REL_TRIGGERS: |
|
256 mixin = MI_REL_TRIGGERS[(rschema, x)] |
|
257 if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ? |
|
258 mixins.append(mixin) |
|
259 for iface in getattr(mixin, '__implements__', ()): |
|
260 if not interface.implements(cls, iface): |
|
261 interface.extend(cls, iface) |
|
262 if x == 'subject': |
|
263 setattr(cls, rschema.type, SubjectRelation(rschema)) |
|
264 else: |
|
265 attr = 'reverse_%s' % rschema.type |
|
266 setattr(cls, attr, ObjectRelation(rschema)) |
|
267 if mixins: |
|
268 cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object]) |
|
269 cls.debug('plugged %s mixins on %s', mixins, etype) |
|
270 cls.rtags.__initialize__() |
|
271 |
|
272 @classmethod |
|
273 def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', |
|
274 settype=True, ordermethod='fetch_order'): |
|
275 """return a rql to fetch all entities of the class type""" |
|
276 restrictions = restriction or [] |
|
277 if settype: |
|
278 restrictions.append('%s is %s' % (mainvar, cls.id)) |
|
279 if fetchattrs is None: |
|
280 fetchattrs = cls.fetch_attrs |
|
281 selection = [mainvar] |
|
282 orderby = [] |
|
283 # start from 26 to avoid possible conflicts with X |
|
284 varmaker = rqlvar_maker(index=26) |
|
285 cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection, |
|
286 orderby, restrictions, user, ordermethod) |
|
287 rql = 'Any %s' % ','.join(selection) |
|
288 if orderby: |
|
289 rql += ' ORDERBY %s' % ','.join(orderby) |
|
290 rql += ' WHERE %s' % ', '.join(restrictions) |
|
291 return rql |
|
292 |
|
293 @classmethod |
|
294 def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs, |
|
295 selection, orderby, restrictions, user, |
|
296 ordermethod='fetch_order', visited=None): |
|
297 eschema = cls.e_schema |
|
298 if visited is None: |
|
299 visited = set((eschema.type,)) |
|
300 elif eschema.type in visited: |
|
301 # avoid infinite recursion |
|
302 return |
|
303 else: |
|
304 visited.add(eschema.type) |
|
305 _fetchattrs = [] |
|
306 for attr in fetchattrs: |
|
307 try: |
|
308 rschema = eschema.subject_relation(attr) |
|
309 except KeyError: |
|
310 cls.warning('skipping fetch_attr %s defined in %s (not found in schema)', |
|
311 attr, cls.id) |
|
312 continue |
|
313 if not user.matching_groups(rschema.get_groups('read')): |
|
314 continue |
|
315 var = varmaker.next() |
|
316 selection.append(var) |
|
317 restriction = '%s %s %s' % (mainvar, attr, var) |
|
318 restrictions.append(restriction) |
|
319 if not rschema.is_final(): |
|
320 # XXX this does not handle several destination types |
|
321 desttype = rschema.objects(eschema.type)[0] |
|
322 card = rschema.rproperty(eschema, desttype, 'cardinality')[0] |
|
323 if card not in '?1': |
|
324 selection.pop() |
|
325 restrictions.pop() |
|
326 continue |
|
327 if card == '?': |
|
328 restrictions[-1] += '?' # left outer join if not mandatory |
|
329 destcls = cls.vreg.etype_class(desttype) |
|
330 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs, |
|
331 selection, orderby, restrictions, |
|
332 user, ordermethod, visited=visited) |
|
333 orderterm = getattr(cls, ordermethod)(attr, var) |
|
334 if orderterm: |
|
335 orderby.append(orderterm) |
|
336 return selection, orderby, restrictions |
|
337 |
|
338 def __init__(self, req, rset, row=None, col=0): |
|
339 AppRsetObject.__init__(self, req, rset) |
|
340 dict.__init__(self) |
|
341 self.row, self.col = row, col |
|
342 self._related_cache = {} |
|
343 if rset is not None: |
|
344 self.eid = rset[row][col] |
|
345 else: |
|
346 self.eid = None |
|
347 self._is_saved = True |
|
348 |
|
349 def __repr__(self): |
|
350 return '<Entity %s %s %s at %s>' % ( |
|
351 self.e_schema, self.eid, self.keys(), id(self)) |
|
352 |
|
353 def __nonzero__(self): |
|
354 return True |
|
355 |
|
356 def __hash__(self): |
|
357 return id(self) |
|
358 |
|
359 def pre_add_hook(self): |
|
360 """hook called by the repository before doing anything to add the entity |
|
361 (before_add entity hooks have not been called yet). This give the |
|
362 occasion to do weird stuff such as autocast (File -> Image for instance). |
|
363 |
|
364 This method must return the actual entity to be added. |
|
365 """ |
|
366 return self |
|
367 |
|
368 def set_eid(self, eid): |
|
369 self.eid = self['eid'] = eid |
|
370 |
|
371 def has_eid(self): |
|
372 """return True if the entity has an attributed eid (False |
|
373 meaning that the entity has to be created |
|
374 """ |
|
375 try: |
|
376 int(self.eid) |
|
377 return True |
|
378 except (ValueError, TypeError): |
|
379 return False |
|
380 |
|
381 def is_saved(self): |
|
382 """during entity creation, there is some time during which the entity |
|
383 has an eid attributed though it's not saved (eg during before_add_entity |
|
384 hooks). You can use this method to ensure the entity has an eid *and* is |
|
385 saved in its source. |
|
386 """ |
|
387 return self.has_eid() and self._is_saved |
|
388 |
|
389 @cached |
|
390 def metainformation(self): |
|
391 res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid))) |
|
392 res['source'] = self.req.source_defs()[res['source']] |
|
393 return res |
|
394 |
|
395 def clear_local_perm_cache(self, action): |
|
396 for rqlexpr in self.e_schema.get_rqlexprs(action): |
|
397 self.req.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None) |
|
398 |
|
399 def check_perm(self, action): |
|
400 self.e_schema.check_perm(self.req, action, self.eid) |
|
401 |
|
402 def has_perm(self, action): |
|
403 return self.e_schema.has_perm(self.req, action, self.eid) |
|
404 |
|
405 def view(self, vid, __registry='views', **kwargs): |
|
406 """shortcut to apply a view on this entity""" |
|
407 return self.vreg.render(__registry, vid, self.req, rset=self.rset, |
|
408 row=self.row, col=self.col, **kwargs) |
|
409 |
|
410 def absolute_url(self, method=None, **kwargs): |
|
411 """return an absolute url to view this entity""" |
|
412 # in linksearch mode, we don't want external urls else selecting |
|
413 # the object for use in the relation is tricky |
|
414 # XXX search_state is web specific |
|
415 if getattr(self.req, 'search_state', ('normal',))[0] == 'normal': |
|
416 kwargs['base_url'] = self.metainformation()['source'].get('base-url') |
|
417 if method is None or method == 'view': |
|
418 kwargs['_restpath'] = self.rest_path() |
|
419 else: |
|
420 kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid |
|
421 return self.build_url(method, **kwargs) |
|
422 |
|
423 def rest_path(self): |
|
424 """returns a REST-like (relative) path for this entity""" |
|
425 mainattr, needcheck = self._rest_attr_info() |
|
426 etype = str(self.e_schema) |
|
427 if mainattr == 'eid': |
|
428 value = self.eid |
|
429 else: |
|
430 value = getattr(self, mainattr) |
|
431 if value is None: |
|
432 return '%s/eid/%s' % (etype.lower(), self.eid) |
|
433 if needcheck: |
|
434 # make sure url is not ambiguous |
|
435 rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (etype, mainattr) |
|
436 if value is not None: |
|
437 nbresults = self.req.execute(rql, {'value' : value})[0][0] |
|
438 # may an assertion that nbresults is not 0 would be a good idea |
|
439 if nbresults != 1: # no ambiguity |
|
440 return '%s/eid/%s' % (etype.lower(), self.eid) |
|
441 return '%s/%s' % (etype.lower(), self.req.url_quote(value)) |
|
442 |
|
443 @classmethod |
|
444 def _rest_attr_info(cls): |
|
445 mainattr, needcheck = 'eid', True |
|
446 if cls.rest_attr: |
|
447 mainattr = cls.rest_attr |
|
448 needcheck = not cls.e_schema.has_unique_values(mainattr) |
|
449 else: |
|
450 for rschema in cls.e_schema.subject_relations(): |
|
451 if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema): |
|
452 mainattr = str(rschema) |
|
453 needcheck = False |
|
454 break |
|
455 if mainattr == 'eid': |
|
456 needcheck = False |
|
457 return mainattr, needcheck |
|
458 |
|
459 @cached |
|
460 def formatted_attrs(self): |
|
461 """returns the list of attributes which have some format information |
|
462 (i.e. rich text strings) |
|
463 """ |
|
464 attrs = [] |
|
465 for rschema, attrschema in self.e_schema.attribute_definitions(): |
|
466 if attrschema.type == 'String' and self.has_format(rschema): |
|
467 attrs.append(rschema.type) |
|
468 return attrs |
|
469 |
|
470 def format(self, attr): |
|
471 """return the mime type format for an attribute (if specified)""" |
|
472 return getattr(self, '%s_format' % attr, None) |
|
473 |
|
474 def text_encoding(self, attr): |
|
475 """return the text encoding for an attribute, default to site encoding |
|
476 """ |
|
477 encoding = getattr(self, '%s_encoding' % attr, None) |
|
478 return encoding or self.vreg.property_value('ui.encoding') |
|
479 |
|
480 def has_format(self, attr): |
|
481 """return true if this entity's schema has a format field for the given |
|
482 attribute |
|
483 """ |
|
484 return self.e_schema.has_subject_relation('%s_format' % attr) |
|
485 |
|
486 def has_text_encoding(self, attr): |
|
487 """return true if this entity's schema has ab encoding field for the |
|
488 given attribute |
|
489 """ |
|
490 return self.e_schema.has_subject_relation('%s_encoding' % attr) |
|
491 |
|
492 def printable_value(self, attr, value=_marker, attrtype=None, |
|
493 format='text/html', displaytime=True): |
|
494 """return a displayable value (i.e. unicode string) which may contains |
|
495 html tags |
|
496 """ |
|
497 attr = str(attr) |
|
498 if value is _marker: |
|
499 value = getattr(self, attr) |
|
500 if isinstance(value, basestring): |
|
501 value = value.strip() |
|
502 if value is None or value == '': # don't use "not", 0 is an acceptable value |
|
503 return u'' |
|
504 if attrtype is None: |
|
505 attrtype = self.e_schema.destination(attr) |
|
506 props = self.e_schema.rproperties(attr) |
|
507 if attrtype == 'String': |
|
508 # internalinalized *and* formatted string such as schema |
|
509 # description... |
|
510 if props.get('internationalizable'): |
|
511 value = self.req._(value) |
|
512 attrformat = self.format(attr) |
|
513 if attrformat: |
|
514 return self.mtc_transform(value, attrformat, format, |
|
515 self.req.encoding) |
|
516 elif attrtype == 'Bytes': |
|
517 attrformat = self.format(attr) |
|
518 if attrformat: |
|
519 try: |
|
520 encoding = getattr(self, '%s_encoding' % attr) |
|
521 except AttributeError: |
|
522 encoding = self.req.encoding |
|
523 return self.mtc_transform(value.getvalue(), attrformat, format, |
|
524 encoding) |
|
525 return u'' |
|
526 value = printable_value(self.req, attrtype, value, props, displaytime) |
|
527 if format == 'text/html': |
|
528 value = html_escape(value) |
|
529 return value |
|
530 |
|
531 def mtc_transform(self, data, format, target_format, encoding, |
|
532 _engine=ENGINE): |
|
533 trdata = TransformData(data, format, encoding, appobject=self) |
|
534 data = _engine.convert(trdata, target_format).decode() |
|
535 if format == 'text/html': |
|
536 data = soup2xhtml(data, self.req.encoding) |
|
537 return data |
|
538 |
|
539 # entity cloning ########################################################## |
|
540 |
|
541 def copy_relations(self, ceid): |
|
542 """copy relations of the object with the given eid on this object |
|
543 |
|
544 By default meta and composite relations are skipped. |
|
545 Overrides this if you want another behaviour |
|
546 """ |
|
547 assert self.has_eid() |
|
548 execute = self.req.execute |
|
549 for rschema in self.e_schema.subject_relations(): |
|
550 if rschema.meta or rschema.is_final(): |
|
551 continue |
|
552 # skip already defined relations |
|
553 if getattr(self, rschema.type): |
|
554 continue |
|
555 if rschema.type in self.skip_copy_for: |
|
556 continue |
|
557 if rschema.type == 'in_state': |
|
558 # if the workflow is defining an initial state (XXX AND we are |
|
559 # not in the managers group? not done to be more consistent) |
|
560 # don't try to copy in_state |
|
561 if execute('Any S WHERE S state_of ET, ET initial_state S,' |
|
562 'ET name %(etype)s', {'etype': str(self.e_schema)}): |
|
563 continue |
|
564 # skip composite relation |
|
565 if self.e_schema.subjrproperty(rschema, 'composite'): |
|
566 continue |
|
567 # skip relation with card in ?1 else we either change the copied |
|
568 # object (inlined relation) or inserting some inconsistency |
|
569 if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1': |
|
570 continue |
|
571 rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % ( |
|
572 rschema.type, rschema.type) |
|
573 execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y')) |
|
574 self.clear_related_cache(rschema.type, 'subject') |
|
575 for rschema in self.e_schema.object_relations(): |
|
576 if rschema.meta: |
|
577 continue |
|
578 # skip already defined relations |
|
579 if getattr(self, 'reverse_%s' % rschema.type): |
|
580 continue |
|
581 # skip composite relation |
|
582 if self.e_schema.objrproperty(rschema, 'composite'): |
|
583 continue |
|
584 # skip relation with card in ?1 else we either change the copied |
|
585 # object (inlined relation) or inserting some inconsistency |
|
586 if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1': |
|
587 continue |
|
588 rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % ( |
|
589 rschema.type, rschema.type) |
|
590 execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y')) |
|
591 self.clear_related_cache(rschema.type, 'object') |
|
592 |
|
593 # data fetching methods ################################################### |
|
594 |
|
595 @cached |
|
596 def as_rset(self): |
|
597 """returns a resultset containing `self` information""" |
|
598 rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s', |
|
599 {'x': self.eid}, [(self.id,)]) |
|
600 return self.req.decorate_rset(rset) |
|
601 |
|
602 def to_complete_relations(self): |
|
603 """by default complete final relations to when calling .complete()""" |
|
604 for rschema in self.e_schema.subject_relations(): |
|
605 if rschema.is_final(): |
|
606 continue |
|
607 if len(rschema.objects(self.e_schema)) > 1: |
|
608 # ambigous relations, the querier doesn't handle |
|
609 # outer join correctly in this case |
|
610 continue |
|
611 if rschema.inlined: |
|
612 matching_groups = self.req.user.matching_groups |
|
613 if matching_groups(rschema.get_groups('read')) and \ |
|
614 all(matching_groups(es.get_groups('read')) |
|
615 for es in rschema.objects(self.e_schema)): |
|
616 yield rschema, 'subject' |
|
617 |
|
618 def to_complete_attributes(self, skip_bytes=True): |
|
619 for rschema, attrschema in self.e_schema.attribute_definitions(): |
|
620 # skip binary data by default |
|
621 if skip_bytes and attrschema.type == 'Bytes': |
|
622 continue |
|
623 attr = rschema.type |
|
624 if attr == 'eid': |
|
625 continue |
|
626 # password retreival is blocked at the repository server level |
|
627 if not self.req.user.matching_groups(rschema.get_groups('read')) \ |
|
628 or attrschema.type == 'Password': |
|
629 self[attr] = None |
|
630 continue |
|
631 yield attr |
|
632 |
|
633 def complete(self, attributes=None, skip_bytes=True): |
|
634 """complete this entity by adding missing attributes (i.e. query the |
|
635 repository to fill the entity) |
|
636 |
|
637 :type skip_bytes: bool |
|
638 :param skip_bytes: |
|
639 if true, attribute of type Bytes won't be considered |
|
640 """ |
|
641 assert self.has_eid() |
|
642 varmaker = rqlvar_maker() |
|
643 V = varmaker.next() |
|
644 rql = ['WHERE %s eid %%(x)s' % V] |
|
645 selected = [] |
|
646 for attr in (attributes or self.to_complete_attributes(skip_bytes)): |
|
647 # if attribute already in entity, nothing to do |
|
648 if self.has_key(attr): |
|
649 continue |
|
650 # case where attribute must be completed, but is not yet in entity |
|
651 var = varmaker.next() |
|
652 rql.append('%s %s %s' % (V, attr, var)) |
|
653 selected.append((attr, var)) |
|
654 # +1 since this doen't include the main variable |
|
655 lastattr = len(selected) + 1 |
|
656 if attributes is None: |
|
657 # fetch additional relations (restricted to 0..1 relations) |
|
658 for rschema, role in self.to_complete_relations(): |
|
659 rtype = rschema.type |
|
660 if self.relation_cached(rtype, role): |
|
661 continue |
|
662 var = varmaker.next() |
|
663 if role == 'subject': |
|
664 targettype = rschema.objects(self.e_schema)[0] |
|
665 card = rschema.rproperty(self.e_schema, targettype, |
|
666 'cardinality')[0] |
|
667 if card == '1': |
|
668 rql.append('%s %s %s' % (V, rtype, var)) |
|
669 else: # '?" |
|
670 rql.append('%s %s %s?' % (V, rtype, var)) |
|
671 else: |
|
672 targettype = rschema.subjects(self.e_schema)[1] |
|
673 card = rschema.rproperty(self.e_schema, targettype, |
|
674 'cardinality')[1] |
|
675 if card == '1': |
|
676 rql.append('%s %s %s' % (var, rtype, V)) |
|
677 else: # '?" |
|
678 rql.append('%s? %s %s' % (var, rtype, V)) |
|
679 assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype, |
|
680 role, card) |
|
681 selected.append(((rtype, role), var)) |
|
682 if selected: |
|
683 # select V, we need it as the left most selected variable |
|
684 # if some outer join are included to fetch inlined relations |
|
685 rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected), |
|
686 ','.join(rql)) |
|
687 execute = getattr(self.req, 'unsafe_execute', self.req.execute) |
|
688 rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0] |
|
689 # handle attributes |
|
690 for i in xrange(1, lastattr): |
|
691 self[str(selected[i-1][0])] = rset[i] |
|
692 # handle relations |
|
693 for i in xrange(lastattr, len(rset)): |
|
694 rtype, x = selected[i-1][0] |
|
695 value = rset[i] |
|
696 if value is None: |
|
697 rrset = ResultSet([], rql, {'x': self.eid}) |
|
698 self.req.decorate_rset(rrset) |
|
699 else: |
|
700 rrset = self.req.eid_rset(value) |
|
701 self.set_related_cache(rtype, x, rrset) |
|
702 |
|
703 def get_value(self, name): |
|
704 """get value for the attribute relation <name>, query the repository |
|
705 to get the value if necessary. |
|
706 |
|
707 :type name: str |
|
708 :param name: name of the attribute to get |
|
709 """ |
|
710 try: |
|
711 value = self[name] |
|
712 except KeyError: |
|
713 if not self.is_saved(): |
|
714 return None |
|
715 rql = "Any A WHERE X eid %%(x)s, X %s A" % name |
|
716 # XXX should we really use unsafe_execute here?? |
|
717 execute = getattr(self.req, 'unsafe_execute', self.req.execute) |
|
718 try: |
|
719 rset = execute(rql, {'x': self.eid}, 'x') |
|
720 except Unauthorized: |
|
721 self[name] = value = None |
|
722 else: |
|
723 assert rset.rowcount <= 1, (self, rql, rset.rowcount) |
|
724 try: |
|
725 self[name] = value = rset.rows[0][0] |
|
726 except IndexError: |
|
727 # probably a multisource error |
|
728 self.critical("can't get value for attribute %s of entity with eid %s", |
|
729 name, self.eid) |
|
730 if self.e_schema.destination(name) == 'String': |
|
731 self[name] = value = self.req._('unaccessible') |
|
732 else: |
|
733 self[name] = value = None |
|
734 return value |
|
735 |
|
736 def related(self, rtype, role='subject', limit=None, entities=False): |
|
737 """returns a resultset of related entities |
|
738 |
|
739 :param role: is the role played by 'self' in the relation ('subject' or 'object') |
|
740 :param limit: resultset's maximum size |
|
741 :param entities: if True, the entites are returned; if False, a result set is returned |
|
742 """ |
|
743 try: |
|
744 return self.related_cache(rtype, role, entities, limit) |
|
745 except KeyError: |
|
746 pass |
|
747 assert self.has_eid() |
|
748 rql = self.related_rql(rtype, role) |
|
749 rset = self.req.execute(rql, {'x': self.eid}, 'x') |
|
750 self.set_related_cache(rtype, role, rset) |
|
751 return self.related(rtype, role, limit, entities) |
|
752 |
|
753 def related_rql(self, rtype, role='subject'): |
|
754 rschema = self.schema[rtype] |
|
755 if role == 'subject': |
|
756 targettypes = rschema.objects(self.e_schema) |
|
757 restriction = 'E eid %%(x)s, E %s X' % rtype |
|
758 card = greater_card(rschema, (self.e_schema,), targettypes, 0) |
|
759 else: |
|
760 targettypes = rschema.subjects(self.e_schema) |
|
761 restriction = 'E eid %%(x)s, X %s E' % rtype |
|
762 card = greater_card(rschema, targettypes, (self.e_schema,), 1) |
|
763 if len(targettypes) > 1: |
|
764 fetchattrs_list = [] |
|
765 for ttype in targettypes: |
|
766 etypecls = self.vreg.etype_class(ttype) |
|
767 fetchattrs_list.append(set(etypecls.fetch_attrs)) |
|
768 fetchattrs = reduce(set.intersection, fetchattrs_list) |
|
769 rql = etypecls.fetch_rql(self.req.user, [restriction], fetchattrs, |
|
770 settype=False) |
|
771 else: |
|
772 etypecls = self.vreg.etype_class(targettypes[0]) |
|
773 rql = etypecls.fetch_rql(self.req.user, [restriction], settype=False) |
|
774 # optimisation: remove ORDERBY if cardinality is 1 or ? (though |
|
775 # greater_card return 1 for those both cases) |
|
776 if card == '1': |
|
777 if ' ORDERBY ' in rql: |
|
778 rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0], |
|
779 rql.split(' WHERE ', 1)[1]) |
|
780 elif not ' ORDERBY ' in rql: |
|
781 args = tuple(rql.split(' WHERE ', 1)) |
|
782 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args |
|
783 return rql |
|
784 |
|
785 # generic vocabulary methods ############################################## |
|
786 |
|
787 def vocabulary(self, rtype, role='subject', limit=None): |
|
788 """vocabulary functions must return a list of couples |
|
789 (label, eid) that will typically be used to fill the |
|
790 edition view's combobox. |
|
791 |
|
792 If `eid` is None in one of these couples, it should be |
|
793 interpreted as a separator in case vocabulary results are grouped |
|
794 """ |
|
795 try: |
|
796 vocabfunc = getattr(self, '%s_%s_vocabulary' % (role, rtype)) |
|
797 except AttributeError: |
|
798 vocabfunc = getattr(self, '%s_relation_vocabulary' % role) |
|
799 # NOTE: it is the responsibility of `vocabfunc` to sort the result |
|
800 # (direclty through RQL or via a python sort). This is also |
|
801 # important because `vocabfunc` might return a list with |
|
802 # couples (label, None) which act as separators. In these |
|
803 # cases, it doesn't make sense to sort results afterwards. |
|
804 return vocabfunc(rtype, limit) |
|
805 |
|
806 def subject_relation_vocabulary(self, rtype, limit=None): |
|
807 """defaut vocabulary method for the given relation, looking for |
|
808 relation's object entities (i.e. self is the subject) |
|
809 """ |
|
810 if isinstance(rtype, basestring): |
|
811 rtype = self.schema.rschema(rtype) |
|
812 done = None |
|
813 assert not rtype.is_final(), rtype |
|
814 if self.has_eid(): |
|
815 done = set(e.eid for e in getattr(self, str(rtype))) |
|
816 result = [] |
|
817 rsetsize = None |
|
818 for objtype in rtype.objects(self.e_schema): |
|
819 if limit is not None: |
|
820 rsetsize = limit - len(result) |
|
821 result += self.relation_vocabulary(rtype, objtype, 'subject', |
|
822 rsetsize, done) |
|
823 if limit is not None and len(result) >= limit: |
|
824 break |
|
825 return result |
|
826 |
|
827 def object_relation_vocabulary(self, rtype, limit=None): |
|
828 """defaut vocabulary method for the given relation, looking for |
|
829 relation's subject entities (i.e. self is the object) |
|
830 """ |
|
831 if isinstance(rtype, basestring): |
|
832 rtype = self.schema.rschema(rtype) |
|
833 done = None |
|
834 if self.has_eid(): |
|
835 done = set(e.eid for e in getattr(self, 'reverse_%s' % rtype)) |
|
836 result = [] |
|
837 rsetsize = None |
|
838 for subjtype in rtype.subjects(self.e_schema): |
|
839 if limit is not None: |
|
840 rsetsize = limit - len(result) |
|
841 result += self.relation_vocabulary(rtype, subjtype, 'object', |
|
842 rsetsize, done) |
|
843 if limit is not None and len(result) >= limit: |
|
844 break |
|
845 return result |
|
846 |
|
847 def relation_vocabulary(self, rtype, targettype, role, |
|
848 limit=None, done=None): |
|
849 if done is None: |
|
850 done = set() |
|
851 req = self.req |
|
852 rset = self.unrelated(rtype, targettype, role, limit) |
|
853 res = [] |
|
854 for entity in rset.entities(): |
|
855 if entity.eid in done: |
|
856 continue |
|
857 done.add(entity.eid) |
|
858 res.append((entity.view('combobox'), entity.eid)) |
|
859 return res |
|
860 |
|
861 def unrelated_rql(self, rtype, targettype, role, ordermethod=None, |
|
862 vocabconstraints=True): |
|
863 """build a rql to fetch `targettype` entities unrelated to this entity |
|
864 using (rtype, role) relation |
|
865 """ |
|
866 ordermethod = ordermethod or 'fetch_unrelated_order' |
|
867 if isinstance(rtype, basestring): |
|
868 rtype = self.schema.rschema(rtype) |
|
869 if role == 'subject': |
|
870 evar, searchedvar = 'S', 'O' |
|
871 subjtype, objtype = self.e_schema, targettype |
|
872 else: |
|
873 searchedvar, evar = 'S', 'O' |
|
874 objtype, subjtype = self.e_schema, targettype |
|
875 if self.has_eid(): |
|
876 restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar] |
|
877 else: |
|
878 restriction = [] |
|
879 constraints = rtype.rproperty(subjtype, objtype, 'constraints') |
|
880 if vocabconstraints: |
|
881 # RQLConstraint is a subclass for RQLVocabularyConstraint, so they |
|
882 # will be included as well |
|
883 restriction += [cstr.restriction for cstr in constraints |
|
884 if isinstance(cstr, RQLVocabularyConstraint)] |
|
885 else: |
|
886 restriction += [cstr.restriction for cstr in constraints |
|
887 if isinstance(cstr, RQLConstraint)] |
|
888 etypecls = self.vreg.etype_class(targettype) |
|
889 rql = etypecls.fetch_rql(self.req.user, restriction, |
|
890 mainvar=searchedvar, ordermethod=ordermethod) |
|
891 # ensure we have an order defined |
|
892 if not ' ORDERBY ' in rql: |
|
893 before, after = rql.split(' WHERE ', 1) |
|
894 rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after) |
|
895 return rql |
|
896 |
|
897 def unrelated(self, rtype, targettype, role='subject', limit=None, |
|
898 ordermethod=None): |
|
899 """return a result set of target type objects that may be related |
|
900 by a given relation, with self as subject or object |
|
901 """ |
|
902 rql = self.unrelated_rql(rtype, targettype, role, ordermethod) |
|
903 if limit is not None: |
|
904 before, after = rql.split(' WHERE ', 1) |
|
905 rql = '%s LIMIT %s WHERE %s' % (before, limit, after) |
|
906 if self.has_eid(): |
|
907 return self.req.execute(rql, {'x': self.eid}) |
|
908 return self.req.execute(rql) |
|
909 |
|
910 # relations cache handling ################################################ |
|
911 |
|
912 def relation_cached(self, rtype, role): |
|
913 """return true if the given relation is already cached on the instance |
|
914 """ |
|
915 return '%s_%s' % (rtype, role) in self._related_cache |
|
916 |
|
917 def related_cache(self, rtype, role, entities=True, limit=None): |
|
918 """return values for the given relation if it's cached on the instance, |
|
919 else raise `KeyError` |
|
920 """ |
|
921 res = self._related_cache['%s_%s' % (rtype, role)][entities] |
|
922 if limit: |
|
923 if entities: |
|
924 res = res[:limit] |
|
925 else: |
|
926 res = res.limit(limit) |
|
927 return res |
|
928 |
|
929 def set_related_cache(self, rtype, role, rset, col=0): |
|
930 """set cached values for the given relation""" |
|
931 if rset: |
|
932 related = list(rset.entities(col)) |
|
933 rschema = self.schema.rschema(rtype) |
|
934 if role == 'subject': |
|
935 rcard = rschema.rproperty(self.e_schema, related[0].e_schema, |
|
936 'cardinality')[1] |
|
937 target = 'object' |
|
938 else: |
|
939 rcard = rschema.rproperty(related[0].e_schema, self.e_schema, |
|
940 'cardinality')[0] |
|
941 target = 'subject' |
|
942 if rcard in '?1': |
|
943 for rentity in related: |
|
944 rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self]) |
|
945 else: |
|
946 related = [] |
|
947 self._related_cache['%s_%s' % (rtype, role)] = (rset, related) |
|
948 |
|
949 def clear_related_cache(self, rtype=None, role=None): |
|
950 """clear cached values for the given relation or the entire cache if |
|
951 no relation is given |
|
952 """ |
|
953 if rtype is None: |
|
954 self._related_cache = {} |
|
955 else: |
|
956 assert role |
|
957 self._related_cache.pop('%s_%s' % (rtype, role), None) |
|
958 |
|
959 # raw edition utilities ################################################### |
|
960 |
|
961 def set_attributes(self, **kwargs): |
|
962 assert kwargs |
|
963 relations = [] |
|
964 for key in kwargs: |
|
965 relations.append('X %s %%(%s)s' % (key, key)) |
|
966 # update current local object |
|
967 self.update(kwargs) |
|
968 # and now update the database |
|
969 kwargs['x'] = self.eid |
|
970 self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), |
|
971 kwargs, 'x') |
|
972 |
|
973 def delete(self): |
|
974 assert self.has_eid(), self.eid |
|
975 self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema, |
|
976 {'x': self.eid}) |
|
977 |
|
978 # server side utilities ################################################### |
|
979 |
|
980 def set_defaults(self): |
|
981 """set default values according to the schema""" |
|
982 self._default_set = set() |
|
983 for attr, value in self.e_schema.defaults(): |
|
984 if not self.has_key(attr): |
|
985 self[str(attr)] = value |
|
986 self._default_set.add(attr) |
|
987 |
|
988 def check(self, creation=False): |
|
989 """check this entity against its schema. Only final relation |
|
990 are checked here, constraint on actual relations are checked in hooks |
|
991 """ |
|
992 # necessary since eid is handled specifically and yams require it to be |
|
993 # in the dictionary |
|
994 if self.req is None: |
|
995 _ = unicode |
|
996 else: |
|
997 _ = self.req._ |
|
998 self.e_schema.check(self, creation=creation, _=_) |
|
999 |
|
1000 def fti_containers(self, _done=None): |
|
1001 if _done is None: |
|
1002 _done = set() |
|
1003 _done.add(self.eid) |
|
1004 containers = tuple(self.e_schema.fulltext_containers()) |
|
1005 if containers: |
|
1006 yielded = False |
|
1007 for rschema, target in containers: |
|
1008 if target == 'object': |
|
1009 targets = getattr(self, rschema.type) |
|
1010 else: |
|
1011 targets = getattr(self, 'reverse_%s' % rschema) |
|
1012 for entity in targets: |
|
1013 if entity.eid in _done: |
|
1014 continue |
|
1015 for container in entity.fti_containers(_done): |
|
1016 yield container |
|
1017 yielded = True |
|
1018 if not yielded: |
|
1019 yield self |
|
1020 else: |
|
1021 yield self |
|
1022 |
|
1023 def get_words(self): |
|
1024 """used by the full text indexer to get words to index |
|
1025 |
|
1026 this method should only be used on the repository side since it depends |
|
1027 on the indexer package |
|
1028 |
|
1029 :rtype: list |
|
1030 :return: the list of indexable word of this entity |
|
1031 """ |
|
1032 from indexer.query_objects import tokenize |
|
1033 words = [] |
|
1034 for rschema in self.e_schema.indexable_attributes(): |
|
1035 try: |
|
1036 value = self.printable_value(rschema, format='text/plain') |
|
1037 except TransformError, ex: |
|
1038 continue |
|
1039 except: |
|
1040 self.exception("can't add value of %s to text index for entity %s", |
|
1041 rschema, self.eid) |
|
1042 continue |
|
1043 if value: |
|
1044 words += tokenize(value) |
|
1045 |
|
1046 for rschema, role in self.e_schema.fulltext_relations(): |
|
1047 if role == 'subject': |
|
1048 for entity in getattr(self, rschema.type): |
|
1049 words += entity.get_words() |
|
1050 else: # if role == 'object': |
|
1051 for entity in getattr(self, 'reverse_%s' % rschema.type): |
|
1052 words += entity.get_words() |
|
1053 return words |
|
1054 |
|
1055 |
|
1056 # attribute and relation descriptors ########################################## |
|
1057 |
|
1058 class Attribute(object): |
|
1059 """descriptor that controls schema attribute access""" |
|
1060 |
|
1061 def __init__(self, attrname): |
|
1062 assert attrname != 'eid' |
|
1063 self._attrname = attrname |
|
1064 |
|
1065 def __get__(self, eobj, eclass): |
|
1066 if eobj is None: |
|
1067 return self |
|
1068 return eobj.get_value(self._attrname) |
|
1069 |
|
1070 def __set__(self, eobj, value): |
|
1071 # XXX bw compat |
|
1072 # would be better to generate UPDATE queries than the current behaviour |
|
1073 eobj.warning("deprecated usage, don't use 'entity.attr = val' notation)") |
|
1074 eobj[self._attrname] = value |
|
1075 |
|
1076 |
|
1077 class Relation(object): |
|
1078 """descriptor that controls schema relation access""" |
|
1079 _role = None # for pylint |
|
1080 |
|
1081 def __init__(self, rschema): |
|
1082 self._rschema = rschema |
|
1083 self._rtype = rschema.type |
|
1084 |
|
1085 def __get__(self, eobj, eclass): |
|
1086 if eobj is None: |
|
1087 raise AttributeError('%s cannot be only be accessed from instances' |
|
1088 % self._rtype) |
|
1089 return eobj.related(self._rtype, self._role, entities=True) |
|
1090 |
|
1091 def __set__(self, eobj, value): |
|
1092 raise NotImplementedError |
|
1093 |
|
1094 |
|
1095 class SubjectRelation(Relation): |
|
1096 """descriptor that controls schema relation access""" |
|
1097 _role = 'subject' |
|
1098 |
|
1099 class ObjectRelation(Relation): |
|
1100 """descriptor that controls schema relation access""" |
|
1101 _role = 'object' |
|
1102 |
|
1103 from logging import getLogger |
|
1104 from cubicweb import set_log_methods |
|
1105 set_log_methods(Entity, getLogger('cubicweb.entity')) |