24 from itertools import chain |
24 from itertools import chain |
25 from warnings import warn |
25 from warnings import warn |
26 |
26 |
27 from logilab.mtconverter import TransformError |
27 from logilab.mtconverter import TransformError |
28 from logilab.common.decorators import cached |
28 from logilab.common.decorators import cached |
29 |
29 from logilab.common.deprecation import class_deprecated |
30 from cubicweb import ValidationError |
30 |
31 from cubicweb.view import EntityAdapter, implements_adapter_compat |
31 from cubicweb import ValidationError, view |
32 from cubicweb.selectors import (implements, is_instance, relation_possible, |
32 from cubicweb.selectors import (implements, is_instance, relation_possible, |
33 match_exception) |
33 match_exception) |
34 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone |
34 from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone |
35 |
35 |
36 |
36 |
37 class IEmailableAdapter(EntityAdapter): |
37 class IEmailableAdapter(view.EntityAdapter): |
38 __regid__ = 'IEmailable' |
38 __regid__ = 'IEmailable' |
39 __select__ = relation_possible('primary_email') | relation_possible('use_email') |
39 __select__ = relation_possible('primary_email') | relation_possible('use_email') |
40 |
40 |
41 def get_email(self): |
41 def get_email(self): |
42 if getattr(self.entity, 'primary_email', None): |
42 if getattr(self.entity, 'primary_email', None): |
65 """ |
65 """ |
66 return dict( (attr, getattr(self.entity, attr)) |
66 return dict( (attr, getattr(self.entity, attr)) |
67 for attr in self.allowed_massmail_keys() ) |
67 for attr in self.allowed_massmail_keys() ) |
68 |
68 |
69 |
69 |
70 class INotifiableAdapter(EntityAdapter): |
70 class INotifiableAdapter(view.EntityAdapter): |
71 __needs_bw_compat__ = True |
71 __needs_bw_compat__ = True |
72 __regid__ = 'INotifiable' |
72 __regid__ = 'INotifiable' |
73 __select__ = is_instance('Any') |
73 __select__ = is_instance('Any') |
74 |
74 |
75 @implements_adapter_compat('INotifiableAdapter') |
75 @view.implements_adapter_compat('INotifiableAdapter') |
76 def notification_references(self, view): |
76 def notification_references(self, view): |
77 """used to control References field of email send on notification |
77 """used to control References field of email send on notification |
78 for this entity. `view` is the notification view. |
78 for this entity. `view` is the notification view. |
79 |
79 |
80 Should return a list of eids which can be used to generate message |
80 Should return a list of eids which can be used to generate message |
154 |
154 |
155 def merge_weight_dict(maindict, newdict): |
155 def merge_weight_dict(maindict, newdict): |
156 for weight, words in newdict.iteritems(): |
156 for weight, words in newdict.iteritems(): |
157 maindict.setdefault(weight, []).extend(words) |
157 maindict.setdefault(weight, []).extend(words) |
158 |
158 |
159 class IDownloadableAdapter(EntityAdapter): |
159 class IDownloadableAdapter(view.EntityAdapter): |
160 """interface for downloadable entities""" |
160 """interface for downloadable entities""" |
161 __needs_bw_compat__ = True |
161 __needs_bw_compat__ = True |
162 __regid__ = 'IDownloadable' |
162 __regid__ = 'IDownloadable' |
163 __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract |
163 __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract |
164 |
164 |
165 @implements_adapter_compat('IDownloadable') |
165 @view.implements_adapter_compat('IDownloadable') |
166 def download_url(self, **kwargs): # XXX not really part of this interface |
166 def download_url(self, **kwargs): # XXX not really part of this interface |
167 """return an url to download entity's content""" |
167 """return an url to download entity's content""" |
168 raise NotImplementedError |
168 raise NotImplementedError |
169 @implements_adapter_compat('IDownloadable') |
169 @view.implements_adapter_compat('IDownloadable') |
170 def download_content_type(self): |
170 def download_content_type(self): |
171 """return MIME type of the downloadable content""" |
171 """return MIME type of the downloadable content""" |
172 raise NotImplementedError |
172 raise NotImplementedError |
173 @implements_adapter_compat('IDownloadable') |
173 @view.implements_adapter_compat('IDownloadable') |
174 def download_encoding(self): |
174 def download_encoding(self): |
175 """return encoding of the downloadable content""" |
175 """return encoding of the downloadable content""" |
176 raise NotImplementedError |
176 raise NotImplementedError |
177 @implements_adapter_compat('IDownloadable') |
177 @view.implements_adapter_compat('IDownloadable') |
178 def download_file_name(self): |
178 def download_file_name(self): |
179 """return file name of the downloadable content""" |
179 """return file name of the downloadable content""" |
180 raise NotImplementedError |
180 raise NotImplementedError |
181 @implements_adapter_compat('IDownloadable') |
181 @view.implements_adapter_compat('IDownloadable') |
182 def download_data(self): |
182 def download_data(self): |
183 """return actual data of the downloadable content""" |
183 """return actual data of the downloadable content""" |
184 raise NotImplementedError |
184 raise NotImplementedError |
185 |
185 |
186 # XXX should propose to use two different relations for children/parent |
186 # XXX should propose to use two different relations for children/parent |
187 class ITreeAdapter(EntityAdapter): |
187 class ITreeAdapter(view.EntityAdapter): |
188 """This adapter has to be overriden to be configured using the |
188 """This adapter has to be overriden to be configured using the |
189 tree_relation, child_role and parent_role class attributes to benefit from |
189 tree_relation, child_role and parent_role class attributes to benefit from |
190 this default implementation. |
190 this default implementation. |
191 |
191 |
192 This adapter provides a tree interface. It has to be overriden to be |
192 This adapter provides a tree interface. It has to be overriden to be |
223 'ITree for %s instead' % (self.entity.__class__), |
223 'ITree for %s instead' % (self.entity.__class__), |
224 DeprecationWarning) |
224 DeprecationWarning) |
225 return self.entity.tree_attribute |
225 return self.entity.tree_attribute |
226 |
226 |
227 # XXX should be removed from the public interface |
227 # XXX should be removed from the public interface |
228 @implements_adapter_compat('ITree') |
228 @view.implements_adapter_compat('ITree') |
229 def children_rql(self): |
229 def children_rql(self): |
230 """Returns RQL to get the children of the entity.""" |
230 """Returns RQL to get the children of the entity.""" |
231 return self.entity.cw_related_rql(self.tree_relation, self.parent_role) |
231 return self.entity.cw_related_rql(self.tree_relation, self.parent_role) |
232 |
232 |
233 @implements_adapter_compat('ITree') |
233 @view.implements_adapter_compat('ITree') |
234 def different_type_children(self, entities=True): |
234 def different_type_children(self, entities=True): |
235 """Return children entities of different type as this entity. |
235 """Return children entities of different type as this entity. |
236 |
236 |
237 According to the `entities` parameter, return entity objects or the |
237 According to the `entities` parameter, return entity objects or the |
238 equivalent result set. |
238 equivalent result set. |
256 eschema = self.entity.e_schema |
256 eschema = self.entity.e_schema |
257 if entities: |
257 if entities: |
258 return [e for e in res if e.e_schema == eschema] |
258 return [e for e in res if e.e_schema == eschema] |
259 return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) |
259 return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) |
260 |
260 |
261 @implements_adapter_compat('ITree') |
261 @view.implements_adapter_compat('ITree') |
262 def is_leaf(self): |
262 def is_leaf(self): |
263 """Returns True if the entity does not have any children.""" |
263 """Returns True if the entity does not have any children.""" |
264 return len(self.children()) == 0 |
264 return len(self.children()) == 0 |
265 |
265 |
266 @implements_adapter_compat('ITree') |
266 @view.implements_adapter_compat('ITree') |
267 def is_root(self): |
267 def is_root(self): |
268 """Returns true if the entity is root of the tree (e.g. has no parent). |
268 """Returns true if the entity is root of the tree (e.g. has no parent). |
269 """ |
269 """ |
270 return self.parent() is None |
270 return self.parent() is None |
271 |
271 |
272 @implements_adapter_compat('ITree') |
272 @view.implements_adapter_compat('ITree') |
273 def root(self): |
273 def root(self): |
274 """Return the root entity of the tree.""" |
274 """Return the root entity of the tree.""" |
275 return self._cw.entity_from_eid(self.path()[0]) |
275 return self._cw.entity_from_eid(self.path()[0]) |
276 |
276 |
277 @implements_adapter_compat('ITree') |
277 @view.implements_adapter_compat('ITree') |
278 def parent(self): |
278 def parent(self): |
279 """Returns the parent entity if any, else None (e.g. if we are on the |
279 """Returns the parent entity if any, else None (e.g. if we are on the |
280 root). |
280 root). |
281 """ |
281 """ |
282 try: |
282 try: |
283 return self.entity.related(self.tree_relation, self.child_role, |
283 return self.entity.related(self.tree_relation, self.child_role, |
284 entities=True)[0] |
284 entities=True)[0] |
285 except (KeyError, IndexError): |
285 except (KeyError, IndexError): |
286 return None |
286 return None |
287 |
287 |
288 @implements_adapter_compat('ITree') |
288 @view.implements_adapter_compat('ITree') |
289 def children(self, entities=True, sametype=False): |
289 def children(self, entities=True, sametype=False): |
290 """Return children entities. |
290 """Return children entities. |
291 |
291 |
292 According to the `entities` parameter, return entity objects or the |
292 According to the `entities` parameter, return entity objects or the |
293 equivalent result set. |
293 equivalent result set. |
361 break |
361 break |
362 path.reverse() |
362 path.reverse() |
363 return path |
363 return path |
364 |
364 |
365 |
365 |
366 class IProgressAdapter(EntityAdapter): |
366 # error handling adapters ###################################################### |
|
367 |
|
368 from cubicweb import UniqueTogetherError |
|
369 |
|
370 class IUserFriendlyError(view.EntityAdapter): |
|
371 __regid__ = 'IUserFriendlyError' |
|
372 __abstract__ = True |
|
373 def __init__(self, *args, **kwargs): |
|
374 self.exc = kwargs.pop('exc') |
|
375 super(IUserFriendlyError, self).__init__(*args, **kwargs) |
|
376 |
|
377 |
|
378 class IUserFriendlyUniqueTogether(IUserFriendlyError): |
|
379 __select__ = match_exception(UniqueTogetherError) |
|
380 def raise_user_exception(self): |
|
381 etype, rtypes = self.exc.args |
|
382 msg = self._cw._('violates unique_together constraints (%s)') % ( |
|
383 ', '.join([self._cw._(rtype) for rtype in rtypes])) |
|
384 raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes)) |
|
385 |
|
386 # deprecated ################################################################### |
|
387 |
|
388 |
|
389 class adapter_deprecated(view.auto_unwrap_bw_compat): |
|
390 """metaclass to print a warning on instantiation of a deprecated class""" |
|
391 |
|
392 def __call__(cls, *args, **kwargs): |
|
393 msg = getattr(cls, "__deprecation_warning__", |
|
394 "%(cls)s is deprecated") % {'cls': cls.__name__} |
|
395 warn(msg, DeprecationWarning, stacklevel=2) |
|
396 return type.__call__(cls, *args, **kwargs) |
|
397 |
|
398 |
|
399 class IProgressAdapter(view.EntityAdapter): |
367 """something that has a cost, a state and a progression. |
400 """something that has a cost, a state and a progression. |
368 |
401 |
369 You should at least override progress_info an in_progress methods on |
402 You should at least override progress_info an in_progress methods on |
370 concrete implementations. |
403 concrete implementations. |
371 """ |
404 """ |
|
405 __metaclass__ = adapter_deprecated |
|
406 __deprecation_warning__ = '[3.14] IProgressAdapter has been moved to iprogress cube' |
372 __needs_bw_compat__ = True |
407 __needs_bw_compat__ = True |
373 __regid__ = 'IProgress' |
408 __regid__ = 'IProgress' |
374 __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract |
409 __select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract |
375 |
410 |
376 @property |
411 @property |
377 @implements_adapter_compat('IProgress') |
412 @view.implements_adapter_compat('IProgress') |
378 def cost(self): |
413 def cost(self): |
379 """the total cost""" |
414 """the total cost""" |
380 return self.progress_info()['estimated'] |
415 return self.progress_info()['estimated'] |
381 |
416 |
382 @property |
417 @property |
383 @implements_adapter_compat('IProgress') |
418 @view.implements_adapter_compat('IProgress') |
384 def revised_cost(self): |
419 def revised_cost(self): |
385 return self.progress_info().get('estimatedcorrected', self.cost) |
420 return self.progress_info().get('estimatedcorrected', self.cost) |
386 |
421 |
387 @property |
422 @property |
388 @implements_adapter_compat('IProgress') |
423 @view.implements_adapter_compat('IProgress') |
389 def done(self): |
424 def done(self): |
390 """what is already done""" |
425 """what is already done""" |
391 return self.progress_info()['done'] |
426 return self.progress_info()['done'] |
392 |
427 |
393 @property |
428 @property |
394 @implements_adapter_compat('IProgress') |
429 @view.implements_adapter_compat('IProgress') |
395 def todo(self): |
430 def todo(self): |
396 """what remains to be done""" |
431 """what remains to be done""" |
397 return self.progress_info()['todo'] |
432 return self.progress_info()['todo'] |
398 |
433 |
399 @implements_adapter_compat('IProgress') |
434 @view.implements_adapter_compat('IProgress') |
400 def progress_info(self): |
435 def progress_info(self): |
401 """returns a dictionary describing progress/estimated cost of the |
436 """returns a dictionary describing progress/estimated cost of the |
402 version. |
437 version. |
403 |
438 |
404 - mandatory keys are (''estimated', 'done', 'todo') |
439 - mandatory keys are (''estimated', 'done', 'todo') |
409 'noestimated' and 'notestimatedcorrected' should default to 0 |
444 'noestimated' and 'notestimatedcorrected' should default to 0 |
410 'estimatedcorrected' should default to 'estimated' |
445 'estimatedcorrected' should default to 'estimated' |
411 """ |
446 """ |
412 raise NotImplementedError |
447 raise NotImplementedError |
413 |
448 |
414 @implements_adapter_compat('IProgress') |
449 @view.implements_adapter_compat('IProgress') |
415 def finished(self): |
450 def finished(self): |
416 """returns True if status is finished""" |
451 """returns True if status is finished""" |
417 return not self.in_progress() |
452 return not self.in_progress() |
418 |
453 |
419 @implements_adapter_compat('IProgress') |
454 @view.implements_adapter_compat('IProgress') |
420 def in_progress(self): |
455 def in_progress(self): |
421 """returns True if status is not finished""" |
456 """returns True if status is not finished""" |
422 raise NotImplementedError |
457 raise NotImplementedError |
423 |
458 |
424 @implements_adapter_compat('IProgress') |
459 @view.implements_adapter_compat('IProgress') |
425 def progress(self): |
460 def progress(self): |
426 """returns the % progress of the task item""" |
461 """returns the % progress of the task item""" |
427 try: |
462 try: |
428 return 100. * self.done / self.revised_cost |
463 return 100. * self.done / self.revised_cost |
429 except ZeroDivisionError: |
464 except ZeroDivisionError: |
430 # total cost is 0 : if everything was estimated, task is completed |
465 # total cost is 0 : if everything was estimated, task is completed |
431 if self.progress_info().get('notestimated'): |
466 if self.progress_info().get('notestimated'): |
432 return 0. |
467 return 0. |
433 return 100 |
468 return 100 |
434 |
469 |
435 @implements_adapter_compat('IProgress') |
470 @view.implements_adapter_compat('IProgress') |
436 def progress_class(self): |
471 def progress_class(self): |
437 return '' |
472 return '' |
438 |
473 |
439 |
474 |
440 class IMileStoneAdapter(IProgressAdapter): |
475 class IMileStoneAdapter(IProgressAdapter): |
|
476 __metaclass__ = adapter_deprecated |
|
477 __deprecation_warning__ = '[3.14] IMileStoneAdapter has been moved to iprogress cube' |
441 __needs_bw_compat__ = True |
478 __needs_bw_compat__ = True |
442 __regid__ = 'IMileStone' |
479 __regid__ = 'IMileStone' |
443 __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract |
480 __select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract |
444 |
481 |
445 parent_type = None # specify main task's type |
482 parent_type = None # specify main task's type |
446 |
483 |
447 @implements_adapter_compat('IMileStone') |
484 @view.implements_adapter_compat('IMileStone') |
448 def get_main_task(self): |
485 def get_main_task(self): |
449 """returns the main ITask entity""" |
486 """returns the main ITask entity""" |
450 raise NotImplementedError |
487 raise NotImplementedError |
451 |
488 |
452 @implements_adapter_compat('IMileStone') |
489 @view.implements_adapter_compat('IMileStone') |
453 def initial_prevision_date(self): |
490 def initial_prevision_date(self): |
454 """returns the initial expected end of the milestone""" |
491 """returns the initial expected end of the milestone""" |
455 raise NotImplementedError |
492 raise NotImplementedError |
456 |
493 |
457 @implements_adapter_compat('IMileStone') |
494 @view.implements_adapter_compat('IMileStone') |
458 def eta_date(self): |
495 def eta_date(self): |
459 """returns expected date of completion based on what remains |
496 """returns expected date of completion based on what remains |
460 to be done |
497 to be done |
461 """ |
498 """ |
462 raise NotImplementedError |
499 raise NotImplementedError |
463 |
500 |
464 @implements_adapter_compat('IMileStone') |
501 @view.implements_adapter_compat('IMileStone') |
465 def completion_date(self): |
502 def completion_date(self): |
466 """returns date on which the subtask has been completed""" |
503 """returns date on which the subtask has been completed""" |
467 raise NotImplementedError |
504 raise NotImplementedError |
468 |
505 |
469 @implements_adapter_compat('IMileStone') |
506 @view.implements_adapter_compat('IMileStone') |
470 def contractors(self): |
507 def contractors(self): |
471 """returns the list of persons supposed to work on this task""" |
508 """returns the list of persons supposed to work on this task""" |
472 raise NotImplementedError |
509 raise NotImplementedError |
473 |
510 |
474 |
|
475 # error handling adapters ###################################################### |
|
476 |
|
477 from cubicweb import UniqueTogetherError |
|
478 |
|
479 class IUserFriendlyError(EntityAdapter): |
|
480 __regid__ = 'IUserFriendlyError' |
|
481 __abstract__ = True |
|
482 def __init__(self, *args, **kwargs): |
|
483 self.exc = kwargs.pop('exc') |
|
484 super(IUserFriendlyError, self).__init__(*args, **kwargs) |
|
485 |
|
486 |
|
487 class IUserFriendlyUniqueTogether(IUserFriendlyError): |
|
488 __select__ = match_exception(UniqueTogetherError) |
|
489 def raise_user_exception(self): |
|
490 etype, rtypes = self.exc.args |
|
491 msg = self._cw._('violates unique_together constraints (%s)') % ( |
|
492 ', '.join([self._cw._(rtype) for rtype in rtypes])) |
|
493 raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes)) |
|