|
1 """base application's entities class implementation: `AnyEntity` |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2001-2008 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 warnings import warn |
|
10 |
|
11 from logilab.common.deprecation import deprecated_function |
|
12 from logilab.common.decorators import cached |
|
13 |
|
14 from cubicweb import Unauthorized, typed_eid |
|
15 from cubicweb.common.utils import dump_class |
|
16 from cubicweb.common.entity import Entity |
|
17 from cubicweb.schema import FormatConstraint |
|
18 |
|
19 from cubicweb.interfaces import IBreadCrumbs |
|
20 |
|
21 class AnyEntity(Entity): |
|
22 """an entity instance has e_schema automagically set on the class and |
|
23 instances have access to their issuing cursor |
|
24 """ |
|
25 id = 'Any' |
|
26 __rtags__ = { |
|
27 'is' : ('generated', 'link'), |
|
28 'is_instance_of' : ('generated', 'link'), |
|
29 'identity' : ('generated', 'link'), |
|
30 |
|
31 # use primary and not generated for eid since it has to be an hidden |
|
32 # field in edition |
|
33 ('eid', '*', 'subject'): 'primary', |
|
34 ('creation_date', '*', 'subject'): 'generated', |
|
35 ('modification_date', '*', 'subject'): 'generated', |
|
36 ('has_text', '*', 'subject'): 'generated', |
|
37 |
|
38 ('require_permission', '*', 'subject') : ('generated', 'link'), |
|
39 ('owned_by', '*', 'subject') : ('generated', 'link'), |
|
40 ('created_by', '*', 'subject') : ('generated', 'link'), |
|
41 |
|
42 ('wf_info_for', '*', 'subject') : ('generated', 'link'), |
|
43 ('wf_info_for', '*', 'object') : ('generated', 'link'), |
|
44 |
|
45 ('description', '*', 'subject'): 'secondary', |
|
46 |
|
47 # XXX should be moved in their respective cubes |
|
48 ('filed_under', '*', 'subject') : ('generic', 'link'), |
|
49 ('filed_under', '*', 'object') : ('generic', 'create'), |
|
50 # generated since there is a componant to handle comments |
|
51 ('comments', '*', 'subject') : ('generated', 'link'), |
|
52 ('comments', '*', 'object') : ('generated', 'link'), |
|
53 } |
|
54 |
|
55 __implements__ = (IBreadCrumbs,) |
|
56 |
|
57 @classmethod |
|
58 def selected(cls, etype): |
|
59 """the special Any entity is used as the default factory, so |
|
60 the actual class has to be constructed at selection time once we |
|
61 have an actual entity'type |
|
62 """ |
|
63 if cls.id == etype: |
|
64 return cls |
|
65 usercls = dump_class(cls, etype) |
|
66 usercls.id = etype |
|
67 usercls.__initialize__() |
|
68 return usercls |
|
69 |
|
70 fetch_attrs = ('modification_date',) |
|
71 @classmethod |
|
72 def fetch_order(cls, attr, var): |
|
73 """class method used to control sort order when multiple entities of |
|
74 this type are fetched |
|
75 """ |
|
76 return cls.fetch_unrelated_order(attr, var) |
|
77 |
|
78 @classmethod |
|
79 def fetch_unrelated_order(cls, attr, var): |
|
80 """class method used to control sort order when multiple entities of |
|
81 this type are fetched to use in edition (eg propose them to create a |
|
82 new relation on an edited entity). |
|
83 """ |
|
84 if attr == 'modification_date': |
|
85 return '%s DESC' % var |
|
86 return None |
|
87 |
|
88 @classmethod |
|
89 def __initialize__(cls): |
|
90 super(ANYENTITY, cls).__initialize__() # XXX |
|
91 eschema = cls.e_schema |
|
92 eschema.format_fields = {} |
|
93 # set a default_ATTR method for rich text format fields |
|
94 for attr, formatattr in eschema.rich_text_fields(): |
|
95 if not hasattr(cls, 'default_%s' % formatattr): |
|
96 setattr(cls, 'default_%s' % formatattr, cls._default_format) |
|
97 eschema.format_fields[formatattr] = attr |
|
98 |
|
99 def _default_format(self): |
|
100 return self.req.property_value('ui.default-text-format') |
|
101 |
|
102 def use_fckeditor(self, attr): |
|
103 """return True if fckeditor should be used to edit entity's attribute named |
|
104 `attr`, according to user preferences |
|
105 """ |
|
106 req = self.req |
|
107 if req.property_value('ui.fckeditor') and self.has_format(attr): |
|
108 if self.has_eid() or '%s_format' % attr in self: |
|
109 return self.format(attr) == 'text/html' |
|
110 return req.property_value('ui.default-text-format') == 'text/html' |
|
111 return False |
|
112 |
|
113 # meta data api ########################################################### |
|
114 |
|
115 def dc_title(self): |
|
116 """return a suitable *unicode* title for this entity""" |
|
117 for rschema, attrschema in self.e_schema.attribute_definitions(): |
|
118 if rschema.meta: |
|
119 continue |
|
120 value = self.get_value(rschema.type) |
|
121 if value: |
|
122 # make the value printable (dates, floats, bytes, etc.) |
|
123 return self.printable_value(rschema.type, value, attrschema.type, |
|
124 format='text/plain') |
|
125 return u'%s #%s' % (self.dc_type(), self.eid) |
|
126 |
|
127 def dc_long_title(self): |
|
128 """return a more detailled title for this entity""" |
|
129 return self.dc_title() |
|
130 |
|
131 def dc_description(self, format='text/plain'): |
|
132 """return a suitable description for this entity""" |
|
133 if hasattr(self, 'description'): |
|
134 return self.printable_value('description', format=format) |
|
135 return u'' |
|
136 |
|
137 def dc_authors(self): |
|
138 """return a suitable description for the author(s) of the entity""" |
|
139 try: |
|
140 return ', '.join(u.name() for u in self.owned_by) |
|
141 except Unauthorized: |
|
142 return u'' |
|
143 |
|
144 def dc_creator(self): |
|
145 """return a suitable description for the creator of the entity""" |
|
146 if self.creator: |
|
147 return self.creator.name() |
|
148 return u'' |
|
149 |
|
150 def dc_date(self, date_format=None):# XXX default to ISO 8601 ? |
|
151 """return latest modification date of this entity""" |
|
152 return self.format_date(self.modification_date, date_format=date_format) |
|
153 |
|
154 def dc_type(self, form=''): |
|
155 """return the display name for the type of this entity (translated)""" |
|
156 return self.e_schema.display_name(self.req, form) |
|
157 display_name = deprecated_function(dc_type) # require agueol > 0.8.1, asteretud > 0.10.0 for removal |
|
158 |
|
159 def dc_language(self): |
|
160 """return language used by this entity (translated)""" |
|
161 # check if entities has internationalizable attributes |
|
162 # XXX one is enough or check if all String attributes are internationalizable? |
|
163 for rschema, attrschema in self.e_schema.attribute_definitions(): |
|
164 if rschema.rproperty(self.e_schema, attrschema, |
|
165 'internationalizable'): |
|
166 return self.req._(self.req.user.property_value('ui.language')) |
|
167 return self.req._(self.vreg.property_value('ui.language')) |
|
168 |
|
169 @property |
|
170 def creator(self): |
|
171 """return the EUser entity which has created this entity, or None if |
|
172 unknown or if the curent user doesn't has access to this euser |
|
173 """ |
|
174 try: |
|
175 return self.created_by[0] |
|
176 except (Unauthorized, IndexError): |
|
177 return None |
|
178 |
|
179 def breadcrumbs(self, view=None, recurs=False): |
|
180 path = [self] |
|
181 if hasattr(self, 'parent'): |
|
182 parent = self.parent() |
|
183 if parent is not None: |
|
184 try: |
|
185 path = parent.breadcrumbs(view, True) + [self] |
|
186 except TypeError: |
|
187 warn("breadcrumbs method's now takes two arguments " |
|
188 "(view=None, recurs=False), please update", |
|
189 DeprecationWarning) |
|
190 path = parent.breadcrumbs(view) + [self] |
|
191 if not recurs: |
|
192 if view is None: |
|
193 if 'vtitle' in self.req.form: |
|
194 # embeding for instance |
|
195 path.append( self.req.form['vtitle'] ) |
|
196 elif view.id != 'primary' and hasattr(view, 'title'): |
|
197 path.append( self.req._(view.title) ) |
|
198 return path |
|
199 |
|
200 # abstractions making the whole things (well, some at least) working ###### |
|
201 |
|
202 @classmethod |
|
203 def get_widget(cls, rschema, x='subject'): |
|
204 """return a widget to view or edit a relation |
|
205 |
|
206 notice that when the relation support multiple target types, the widget |
|
207 is necessarily the same for all those types |
|
208 """ |
|
209 # let ImportError propage if web par isn't available |
|
210 from cubicweb.web.widgets import widget |
|
211 if isinstance(rschema, basestring): |
|
212 rschema = cls.schema.rschema(rschema) |
|
213 if x == 'subject': |
|
214 tschema = rschema.objects(cls.e_schema)[0] |
|
215 wdg = widget(cls.vreg, cls, rschema, tschema, 'subject') |
|
216 else: |
|
217 tschema = rschema.subjects(cls.e_schema)[0] |
|
218 wdg = widget(cls.vreg, tschema, rschema, cls, 'object') |
|
219 return wdg |
|
220 |
|
221 def sortvalue(self, rtype=None): |
|
222 """return a value which can be used to sort this entity or given |
|
223 entity's attribute |
|
224 """ |
|
225 if rtype is None: |
|
226 return self.dc_title().lower() |
|
227 value = self.get_value(rtype) |
|
228 # do not restrict to `unicode` because Bytes will return a `str` value |
|
229 if isinstance(value, basestring): |
|
230 return self.printable_value(rtype, format='text/plain').lower() |
|
231 return value |
|
232 |
|
233 def after_deletion_path(self): |
|
234 """return (path, parameters) which should be used as redirect |
|
235 information when this entity is being deleted |
|
236 """ |
|
237 return str(self.e_schema).lower(), {} |
|
238 |
|
239 def add_related_schemas(self): |
|
240 """this is actually used ui method to generate 'addrelated' actions from |
|
241 the schema. |
|
242 |
|
243 If you're using explicit 'addrelated' actions for an entity types, you |
|
244 should probably overrides this method to return an empty list else you |
|
245 may get some unexpected actions. |
|
246 """ |
|
247 req = self.req |
|
248 eschema = self.e_schema |
|
249 for role, rschemas in (('subject', eschema.subject_relations()), |
|
250 ('object', eschema.object_relations())): |
|
251 for rschema in rschemas: |
|
252 if rschema.is_final(): |
|
253 continue |
|
254 # check the relation can be added as well |
|
255 if role == 'subject'and not rschema.has_perm(req, 'add', fromeid=self.eid): |
|
256 continue |
|
257 if role == 'object'and not rschema.has_perm(req, 'add', toeid=self.eid): |
|
258 continue |
|
259 # check the target types can be added as well |
|
260 for teschema in rschema.targets(eschema, role): |
|
261 if not self.relation_mode(rschema, teschema, role) == 'create': |
|
262 continue |
|
263 if teschema.has_local_role('add') or teschema.has_perm(req, 'add'): |
|
264 yield rschema, teschema, role |
|
265 |
|
266 def relation_mode(self, rtype, targettype, role='subject'): |
|
267 """return a string telling if the given relation is usually created |
|
268 to a new entity ('create' mode) or to an existant entity ('link' mode) |
|
269 """ |
|
270 return self.rtags.get_mode(rtype, targettype, role) |
|
271 |
|
272 # edition helper functions ################################################ |
|
273 |
|
274 def relations_by_category(self, categories=None, permission=None): |
|
275 if categories is not None: |
|
276 if not isinstance(categories, (list, tuple, set, frozenset)): |
|
277 categories = (categories,) |
|
278 if not isinstance(categories, (set, frozenset)): |
|
279 categories = frozenset(categories) |
|
280 eschema, rtags = self.e_schema, self.rtags |
|
281 if self.has_eid(): |
|
282 eid = self.eid |
|
283 else: |
|
284 eid = None |
|
285 for rschema, targetschemas, role in eschema.relation_definitions(True): |
|
286 if rschema in ('identity', 'has_text'): |
|
287 continue |
|
288 # check category first, potentially lower cost than checking |
|
289 # permission which may imply rql queries |
|
290 if categories is not None: |
|
291 targetschemas = [tschema for tschema in targetschemas |
|
292 if rtags.get_tags(rschema.type, tschema.type, role).intersection(categories)] |
|
293 if not targetschemas: |
|
294 continue |
|
295 tags = rtags.get_tags(rschema.type, role=role) |
|
296 if permission is not None: |
|
297 # tag allowing to hijack the permission machinery when |
|
298 # permission is not verifiable until the entity is actually |
|
299 # created... |
|
300 if eid is None and ('%s_on_new' % permission) in tags: |
|
301 yield (rschema, targetschemas, role) |
|
302 continue |
|
303 if rschema.is_final(): |
|
304 if not rschema.has_perm(self.req, permission, eid): |
|
305 continue |
|
306 elif role == 'subject': |
|
307 if not ((eid is None and rschema.has_local_role(permission)) or |
|
308 rschema.has_perm(self.req, permission, fromeid=eid)): |
|
309 continue |
|
310 # on relation with cardinality 1 or ?, we need delete perm as well |
|
311 # if the relation is already set |
|
312 if (permission == 'add' |
|
313 and rschema.cardinality(eschema, targetschemas[0], role) in '1?' |
|
314 and self.has_eid() and self.related(rschema.type, role) |
|
315 and not rschema.has_perm(self.req, 'delete', fromeid=eid, |
|
316 toeid=self.related(rschema.type, role)[0][0])): |
|
317 continue |
|
318 elif role == 'object': |
|
319 if not ((eid is None and rschema.has_local_role(permission)) or |
|
320 rschema.has_perm(self.req, permission, toeid=eid)): |
|
321 continue |
|
322 # on relation with cardinality 1 or ?, we need delete perm as well |
|
323 # if the relation is already set |
|
324 if (permission == 'add' |
|
325 and rschema.cardinality(targetschemas[0], eschema, role) in '1?' |
|
326 and self.has_eid() and self.related(rschema.type, role) |
|
327 and not rschema.has_perm(self.req, 'delete', toeid=eid, |
|
328 fromeid=self.related(rschema.type, role)[0][0])): |
|
329 continue |
|
330 yield (rschema, targetschemas, role) |
|
331 |
|
332 def srelations_by_category(self, categories=None, permission=None): |
|
333 result = [] |
|
334 for rschema, ttypes, target in self.relations_by_category(categories, |
|
335 permission): |
|
336 if rschema.is_final(): |
|
337 continue |
|
338 result.append( (rschema.display_name(self.req, target), rschema, target) ) |
|
339 return sorted(result) |
|
340 |
|
341 def attribute_values(self, attrname): |
|
342 if self.has_eid() or attrname in self: |
|
343 try: |
|
344 values = self[attrname] |
|
345 except KeyError: |
|
346 values = getattr(self, attrname) |
|
347 # actual relation return a list of entities |
|
348 if isinstance(values, list): |
|
349 return [v.eid for v in values] |
|
350 return (values,) |
|
351 # the entity is being created, try to find default value for |
|
352 # this attribute |
|
353 try: |
|
354 values = self.req.form[attrname] |
|
355 except KeyError: |
|
356 try: |
|
357 values = self[attrname] # copying |
|
358 except KeyError: |
|
359 values = getattr(self, 'default_%s' % attrname, |
|
360 self.e_schema.default(attrname)) |
|
361 if callable(values): |
|
362 values = values() |
|
363 if values is None: |
|
364 values = () |
|
365 elif not isinstance(values, (list, tuple)): |
|
366 values = (values,) |
|
367 return values |
|
368 |
|
369 def linked_to(self, rtype, target, remove=True): |
|
370 """if entity should be linked to another using __linkto form param for |
|
371 the given relation/target, return eids of related entities |
|
372 |
|
373 This method is consuming matching link-to information from form params |
|
374 if `remove` is True (by default). |
|
375 """ |
|
376 try: |
|
377 return self.__linkto[(rtype, target)] |
|
378 except AttributeError: |
|
379 self.__linkto = {} |
|
380 except KeyError: |
|
381 pass |
|
382 linktos = list(self.req.list_form_param('__linkto')) |
|
383 linkedto = [] |
|
384 for linkto in linktos[:]: |
|
385 ltrtype, eid, lttarget = linkto.split(':') |
|
386 if rtype == ltrtype and target == lttarget: |
|
387 # delete __linkto from form param to avoid it being added as |
|
388 # hidden input |
|
389 if remove: |
|
390 linktos.remove(linkto) |
|
391 self.req.form['__linkto'] = linktos |
|
392 linkedto.append(typed_eid(eid)) |
|
393 self.__linkto[(rtype, target)] = linkedto |
|
394 return linkedto |
|
395 |
|
396 def pre_web_edit(self): |
|
397 """callback called by the web editcontroller when an entity will be |
|
398 created/modified, to let a chance to do some entity specific stuff. |
|
399 |
|
400 Do nothing by default. |
|
401 """ |
|
402 pass |
|
403 |
|
404 # server side helpers ##################################################### |
|
405 |
|
406 def notification_references(self, view): |
|
407 """used to control References field of email send on notification |
|
408 for this entity. `view` is the notification view. |
|
409 |
|
410 Should return a list of eids which can be used to generate message ids |
|
411 of previously sent email |
|
412 """ |
|
413 return () |
|
414 |
|
415 # XXX: store a reference to the AnyEntity class since it is hijacked in goa |
|
416 # configuration and we need the actual reference to avoid infinite loops |
|
417 # in mro |
|
418 ANYENTITY = AnyEntity |
|
419 |
|
420 def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'): |
|
421 if pclass is ANYENTITY: |
|
422 pclass = AnyEntity # AnyEntity and ANYENTITY may be different classes |
|
423 if pclass is not None: |
|
424 fetchattrs += pclass.fetch_attrs |
|
425 if mainattr is None: |
|
426 mainattr = fetchattrs[0] |
|
427 @classmethod |
|
428 def fetch_order(cls, attr, var): |
|
429 if attr == mainattr: |
|
430 return '%s %s' % (var, order) |
|
431 return None |
|
432 return fetchattrs, fetch_order |