1 # copyright 2010-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """some basic entity adapter implementations, for interfaces used in the |
|
19 framework itself. |
|
20 """ |
|
21 from cubicweb import _ |
|
22 |
|
23 from itertools import chain |
|
24 from hashlib import md5 |
|
25 |
|
26 from logilab.mtconverter import TransformError |
|
27 from logilab.common.decorators import cached |
|
28 |
|
29 from cubicweb import ValidationError, view, ViolatedConstraint, UniqueTogetherError |
|
30 from cubicweb.predicates import is_instance, relation_possible, match_exception |
|
31 |
|
32 |
|
33 class IEmailableAdapter(view.EntityAdapter): |
|
34 __regid__ = 'IEmailable' |
|
35 __select__ = relation_possible('primary_email') | relation_possible('use_email') |
|
36 |
|
37 def get_email(self): |
|
38 if getattr(self.entity, 'primary_email', None): |
|
39 return self.entity.primary_email[0].address |
|
40 if getattr(self.entity, 'use_email', None): |
|
41 return self.entity.use_email[0].address |
|
42 return None |
|
43 |
|
44 def allowed_massmail_keys(self): |
|
45 """returns a set of allowed email substitution keys |
|
46 |
|
47 The default is to return the entity's attribute list but you might |
|
48 override this method to allow extra keys. For instance, a Person |
|
49 class might want to return a `companyname` key. |
|
50 """ |
|
51 return set(rschema.type |
|
52 for rschema, attrtype in self.entity.e_schema.attribute_definitions() |
|
53 if attrtype.type not in ('Password', 'Bytes')) |
|
54 |
|
55 def as_email_context(self): |
|
56 """returns the dictionary as used by the sendmail controller to |
|
57 build email bodies. |
|
58 |
|
59 NOTE: the dictionary keys should match the list returned by the |
|
60 `allowed_massmail_keys` method. |
|
61 """ |
|
62 return dict((attr, getattr(self.entity, attr)) |
|
63 for attr in self.allowed_massmail_keys()) |
|
64 |
|
65 |
|
66 class INotifiableAdapter(view.EntityAdapter): |
|
67 __regid__ = 'INotifiable' |
|
68 __select__ = is_instance('Any') |
|
69 |
|
70 def notification_references(self, view): |
|
71 """used to control References field of email send on notification |
|
72 for this entity. `view` is the notification view. |
|
73 |
|
74 Should return a list of eids which can be used to generate message |
|
75 identifiers of previously sent email(s) |
|
76 """ |
|
77 itree = self.entity.cw_adapt_to('ITree') |
|
78 if itree is not None: |
|
79 return itree.path()[:-1] |
|
80 if view.msgid_timestamp: |
|
81 return (self.entity.eid,) |
|
82 return () |
|
83 |
|
84 |
|
85 class IFTIndexableAdapter(view.EntityAdapter): |
|
86 """standard adapter to handle fulltext indexing |
|
87 |
|
88 .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers |
|
89 .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words |
|
90 """ |
|
91 __regid__ = 'IFTIndexable' |
|
92 __select__ = is_instance('Any') |
|
93 |
|
94 def fti_containers(self, _done=None): |
|
95 """return the list of entities to index when handling ``self.entity`` |
|
96 |
|
97 The actual list of entities depends on ``fulltext_container`` usage |
|
98 in the datamodel definition |
|
99 """ |
|
100 if _done is None: |
|
101 _done = set() |
|
102 entity = self.entity |
|
103 _done.add(entity.eid) |
|
104 containers = tuple(entity.e_schema.fulltext_containers()) |
|
105 if containers: |
|
106 for rschema, role in containers: |
|
107 if role == 'object': |
|
108 targets = getattr(entity, rschema.type) |
|
109 else: |
|
110 targets = getattr(entity, 'reverse_%s' % rschema) |
|
111 for target in targets: |
|
112 if target.eid in _done: |
|
113 continue |
|
114 for container in target.cw_adapt_to('IFTIndexable').fti_containers(_done): |
|
115 yield container |
|
116 else: |
|
117 yield entity |
|
118 |
|
119 # weight in ABCD |
|
120 entity_weight = 1.0 |
|
121 attr_weight = {} |
|
122 |
|
123 def get_words(self): |
|
124 """used by the full text indexer to get words to index |
|
125 |
|
126 this method should only be used on the repository side since it depends |
|
127 on the logilab.database package |
|
128 |
|
129 :rtype: list |
|
130 :return: the list of indexable word of this entity |
|
131 """ |
|
132 from logilab.database.fti import tokenize |
|
133 # take care to cases where we're modyfying the schema |
|
134 entity = self.entity |
|
135 pending = self._cw.transaction_data.setdefault('pendingrdefs', set()) |
|
136 words = {} |
|
137 for rschema in entity.e_schema.indexable_attributes(): |
|
138 if (entity.e_schema, rschema) in pending: |
|
139 continue |
|
140 weight = self.attr_weight.get(rschema, 'C') |
|
141 try: |
|
142 value = entity.printable_value(rschema, format=u'text/plain') |
|
143 except TransformError: |
|
144 continue |
|
145 except Exception: |
|
146 self.exception("can't add value of %s to text index for entity %s", |
|
147 rschema, entity.eid) |
|
148 continue |
|
149 if value: |
|
150 words.setdefault(weight, []).extend(tokenize(value)) |
|
151 for rschema, role in entity.e_schema.fulltext_relations(): |
|
152 if role == 'subject': |
|
153 for entity_ in getattr(entity, rschema.type): |
|
154 merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words()) |
|
155 else: # if role == 'object': |
|
156 for entity_ in getattr(entity, 'reverse_%s' % rschema.type): |
|
157 merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words()) |
|
158 return words |
|
159 |
|
160 |
|
161 def merge_weight_dict(maindict, newdict): |
|
162 for weight, words in newdict.items(): |
|
163 maindict.setdefault(weight, []).extend(words) |
|
164 |
|
165 |
|
166 class IDownloadableAdapter(view.EntityAdapter): |
|
167 """interface for downloadable entities""" |
|
168 __regid__ = 'IDownloadable' |
|
169 __abstract__ = True |
|
170 |
|
171 def download_url(self, **kwargs): # XXX not really part of this interface |
|
172 """return a URL to download entity's content |
|
173 |
|
174 It should be a unicode object containing url-encoded ASCII. |
|
175 """ |
|
176 raise NotImplementedError |
|
177 |
|
178 def download_content_type(self): |
|
179 """return MIME type (unicode) of the downloadable content""" |
|
180 raise NotImplementedError |
|
181 |
|
182 def download_encoding(self): |
|
183 """return encoding (unicode) of the downloadable content""" |
|
184 raise NotImplementedError |
|
185 |
|
186 def download_file_name(self): |
|
187 """return file name (unicode) of the downloadable content""" |
|
188 raise NotImplementedError |
|
189 |
|
190 def download_data(self): |
|
191 """return actual data (bytes) of the downloadable content""" |
|
192 raise NotImplementedError |
|
193 |
|
194 |
|
195 # XXX should propose to use two different relations for children/parent |
|
196 class ITreeAdapter(view.EntityAdapter): |
|
197 """This adapter provides a tree interface. |
|
198 |
|
199 It has to be overriden to be configured using the tree_relation, |
|
200 child_role and parent_role class attributes to benefit from this default |
|
201 implementation. |
|
202 |
|
203 This class provides the following methods: |
|
204 |
|
205 .. automethod: iterparents |
|
206 .. automethod: iterchildren |
|
207 .. automethod: prefixiter |
|
208 |
|
209 .. automethod: is_leaf |
|
210 .. automethod: is_root |
|
211 |
|
212 .. automethod: root |
|
213 .. automethod: parent |
|
214 .. automethod: children |
|
215 .. automethod: different_type_children |
|
216 .. automethod: same_type_children |
|
217 .. automethod: children_rql |
|
218 .. automethod: path |
|
219 """ |
|
220 __regid__ = 'ITree' |
|
221 __abstract__ = True |
|
222 |
|
223 child_role = 'subject' |
|
224 parent_role = 'object' |
|
225 |
|
226 def children_rql(self): |
|
227 """Returns RQL to get the children of the entity.""" |
|
228 return self.entity.cw_related_rql(self.tree_relation, self.parent_role) |
|
229 |
|
230 def different_type_children(self, entities=True): |
|
231 """Return children entities of different type as this entity. |
|
232 |
|
233 According to the `entities` parameter, return entity objects or the |
|
234 equivalent result set. |
|
235 """ |
|
236 res = self.entity.related(self.tree_relation, self.parent_role, |
|
237 entities=entities) |
|
238 eschema = self.entity.e_schema |
|
239 if entities: |
|
240 return [e for e in res if e.e_schema != eschema] |
|
241 return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col) |
|
242 |
|
243 def same_type_children(self, entities=True): |
|
244 """Return children entities of the same type as this entity. |
|
245 |
|
246 According to the `entities` parameter, return entity objects or the |
|
247 equivalent result set. |
|
248 """ |
|
249 res = self.entity.related(self.tree_relation, self.parent_role, |
|
250 entities=entities) |
|
251 eschema = self.entity.e_schema |
|
252 if entities: |
|
253 return [e for e in res if e.e_schema == eschema] |
|
254 return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) |
|
255 |
|
256 def is_leaf(self): |
|
257 """Returns True if the entity does not have any children.""" |
|
258 return len(self.children()) == 0 |
|
259 |
|
260 def is_root(self): |
|
261 """Returns true if the entity is root of the tree (e.g. has no parent). |
|
262 """ |
|
263 return self.parent() is None |
|
264 |
|
265 def root(self): |
|
266 """Return the root entity of the tree.""" |
|
267 return self._cw.entity_from_eid(self.path()[0]) |
|
268 |
|
269 def parent(self): |
|
270 """Returns the parent entity if any, else None (e.g. if we are on the |
|
271 root). |
|
272 """ |
|
273 try: |
|
274 return self.entity.related(self.tree_relation, self.child_role, |
|
275 entities=True)[0] |
|
276 except (KeyError, IndexError): |
|
277 return None |
|
278 |
|
279 def children(self, entities=True, sametype=False): |
|
280 """Return children entities. |
|
281 |
|
282 According to the `entities` parameter, return entity objects or the |
|
283 equivalent result set. |
|
284 """ |
|
285 if sametype: |
|
286 return self.same_type_children(entities) |
|
287 else: |
|
288 return self.entity.related(self.tree_relation, self.parent_role, |
|
289 entities=entities) |
|
290 |
|
291 def iterparents(self, strict=True): |
|
292 """Return an iterator on the parents of the entity.""" |
|
293 def _uptoroot(self): |
|
294 curr = self |
|
295 while True: |
|
296 curr = curr.parent() |
|
297 if curr is None: |
|
298 break |
|
299 yield curr |
|
300 curr = curr.cw_adapt_to('ITree') |
|
301 if not strict: |
|
302 return chain([self.entity], _uptoroot(self)) |
|
303 return _uptoroot(self) |
|
304 |
|
305 def iterchildren(self, _done=None): |
|
306 """Return an iterator over the item's children.""" |
|
307 if _done is None: |
|
308 _done = set() |
|
309 for child in self.children(): |
|
310 if child.eid in _done: |
|
311 self.error('loop in %s tree: %s', child.cw_etype.lower(), child) |
|
312 continue |
|
313 yield child |
|
314 _done.add(child.eid) |
|
315 |
|
316 def prefixiter(self, _done=None): |
|
317 """Return an iterator over the item's descendants in a prefixed order.""" |
|
318 if _done is None: |
|
319 _done = set() |
|
320 if self.entity.eid in _done: |
|
321 return |
|
322 _done.add(self.entity.eid) |
|
323 yield self.entity |
|
324 for child in self.same_type_children(): |
|
325 for entity in child.cw_adapt_to('ITree').prefixiter(_done): |
|
326 yield entity |
|
327 |
|
328 @cached |
|
329 def path(self): |
|
330 """Returns the list of eids from the root object to this object.""" |
|
331 path = [] |
|
332 adapter = self |
|
333 entity = adapter.entity |
|
334 while entity is not None: |
|
335 if entity.eid in path: |
|
336 self.error('loop in %s tree: %s', entity.cw_etype.lower(), entity) |
|
337 break |
|
338 path.append(entity.eid) |
|
339 try: |
|
340 # check we are not jumping to another tree |
|
341 if (adapter.tree_relation != self.tree_relation or |
|
342 adapter.child_role != self.child_role): |
|
343 break |
|
344 entity = adapter.parent() |
|
345 adapter = entity.cw_adapt_to('ITree') |
|
346 except AttributeError: |
|
347 break |
|
348 path.reverse() |
|
349 return path |
|
350 |
|
351 |
|
352 class ISerializableAdapter(view.EntityAdapter): |
|
353 """Adapter to serialize an entity to a bare python structure that may be |
|
354 directly serialized to e.g. JSON. |
|
355 """ |
|
356 |
|
357 __regid__ = 'ISerializable' |
|
358 __select__ = is_instance('Any') |
|
359 |
|
360 def serialize(self): |
|
361 entity = self.entity |
|
362 entity.complete() |
|
363 data = { |
|
364 'cw_etype': entity.cw_etype, |
|
365 'cw_source': entity.cw_metainformation()['source']['uri'], |
|
366 'eid': entity.eid, |
|
367 } |
|
368 for rschema, __ in entity.e_schema.attribute_definitions(): |
|
369 attr = rschema.type |
|
370 try: |
|
371 value = entity.cw_attr_cache[attr] |
|
372 except KeyError: |
|
373 # Bytes |
|
374 continue |
|
375 data[attr] = value |
|
376 return data |
|
377 |
|
378 |
|
379 # error handling adapters ###################################################### |
|
380 |
|
381 |
|
382 class IUserFriendlyError(view.EntityAdapter): |
|
383 __regid__ = 'IUserFriendlyError' |
|
384 __abstract__ = True |
|
385 |
|
386 def __init__(self, *args, **kwargs): |
|
387 self.exc = kwargs.pop('exc') |
|
388 super(IUserFriendlyError, self).__init__(*args, **kwargs) |
|
389 |
|
390 |
|
391 class IUserFriendlyUniqueTogether(IUserFriendlyError): |
|
392 __select__ = match_exception(UniqueTogetherError) |
|
393 |
|
394 def raise_user_exception(self): |
|
395 rtypes = self.exc.rtypes |
|
396 errors = {} |
|
397 msgargs = {} |
|
398 i18nvalues = [] |
|
399 for rtype in rtypes: |
|
400 errors[rtype] = _('%(KEY-rtype)s is part of violated unicity constraint') |
|
401 msgargs[rtype + '-rtype'] = rtype |
|
402 i18nvalues.append(rtype + '-rtype') |
|
403 errors[''] = _('some relations violate a unicity constraint') |
|
404 raise ValidationError(self.entity.eid, errors, msgargs=msgargs, i18nvalues=i18nvalues) |
|
405 |
|
406 |
|
407 class IUserFriendlyCheckConstraint(IUserFriendlyError): |
|
408 __select__ = match_exception(ViolatedConstraint) |
|
409 |
|
410 def raise_user_exception(self): |
|
411 cstrname = self.exc.cstrname |
|
412 eschema = self.entity.e_schema |
|
413 for rschema, attrschema in eschema.attribute_definitions(): |
|
414 rdef = rschema.rdef(eschema, attrschema) |
|
415 for constraint in rdef.constraints: |
|
416 if cstrname == 'cstr' + md5( |
|
417 (eschema.type + rschema.type + constraint.type() + |
|
418 (constraint.serialize() or '')).encode('ascii')).hexdigest(): |
|
419 break |
|
420 else: |
|
421 continue |
|
422 break |
|
423 else: |
|
424 assert 0 |
|
425 key = rschema.type + '-subject' |
|
426 msg, args = constraint.failed_message(key, self.entity.cw_edited[rschema.type]) |
|
427 raise ValidationError(self.entity.eid, {key: msg}, args) |
|