--- a/entities/adapters.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,427 +0,0 @@
-# copyright 2010-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""some basic entity adapter implementations, for interfaces used in the
-framework itself.
-"""
-from cubicweb import _
-
-from itertools import chain
-from hashlib import md5
-
-from logilab.mtconverter import TransformError
-from logilab.common.decorators import cached
-
-from cubicweb import ValidationError, view, ViolatedConstraint, UniqueTogetherError
-from cubicweb.predicates import is_instance, relation_possible, match_exception
-
-
-class IEmailableAdapter(view.EntityAdapter):
- __regid__ = 'IEmailable'
- __select__ = relation_possible('primary_email') | relation_possible('use_email')
-
- def get_email(self):
- if getattr(self.entity, 'primary_email', None):
- return self.entity.primary_email[0].address
- if getattr(self.entity, 'use_email', None):
- return self.entity.use_email[0].address
- return None
-
- def allowed_massmail_keys(self):
- """returns a set of allowed email substitution keys
-
- The default is to return the entity's attribute list but you might
- override this method to allow extra keys. For instance, a Person
- class might want to return a `companyname` key.
- """
- return set(rschema.type
- for rschema, attrtype in self.entity.e_schema.attribute_definitions()
- if attrtype.type not in ('Password', 'Bytes'))
-
- def as_email_context(self):
- """returns the dictionary as used by the sendmail controller to
- build email bodies.
-
- NOTE: the dictionary keys should match the list returned by the
- `allowed_massmail_keys` method.
- """
- return dict((attr, getattr(self.entity, attr))
- for attr in self.allowed_massmail_keys())
-
-
-class INotifiableAdapter(view.EntityAdapter):
- __regid__ = 'INotifiable'
- __select__ = is_instance('Any')
-
- def notification_references(self, view):
- """used to control References field of email send on notification
- for this entity. `view` is the notification view.
-
- Should return a list of eids which can be used to generate message
- identifiers of previously sent email(s)
- """
- itree = self.entity.cw_adapt_to('ITree')
- if itree is not None:
- return itree.path()[:-1]
- if view.msgid_timestamp:
- return (self.entity.eid,)
- return ()
-
-
-class IFTIndexableAdapter(view.EntityAdapter):
- """standard adapter to handle fulltext indexing
-
- .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
- .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
- """
- __regid__ = 'IFTIndexable'
- __select__ = is_instance('Any')
-
- def fti_containers(self, _done=None):
- """return the list of entities to index when handling ``self.entity``
-
- The actual list of entities depends on ``fulltext_container`` usage
- in the datamodel definition
- """
- if _done is None:
- _done = set()
- entity = self.entity
- _done.add(entity.eid)
- containers = tuple(entity.e_schema.fulltext_containers())
- if containers:
- for rschema, role in containers:
- if role == 'object':
- targets = getattr(entity, rschema.type)
- else:
- targets = getattr(entity, 'reverse_%s' % rschema)
- for target in targets:
- if target.eid in _done:
- continue
- for container in target.cw_adapt_to('IFTIndexable').fti_containers(_done):
- yield container
- else:
- yield entity
-
- # weight in ABCD
- entity_weight = 1.0
- attr_weight = {}
-
- def get_words(self):
- """used by the full text indexer to get words to index
-
- this method should only be used on the repository side since it depends
- on the logilab.database package
-
- :rtype: list
- :return: the list of indexable word of this entity
- """
- from logilab.database.fti import tokenize
- # take care to cases where we're modyfying the schema
- entity = self.entity
- pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
- words = {}
- for rschema in entity.e_schema.indexable_attributes():
- if (entity.e_schema, rschema) in pending:
- continue
- weight = self.attr_weight.get(rschema, 'C')
- try:
- value = entity.printable_value(rschema, format=u'text/plain')
- except TransformError:
- continue
- except Exception:
- self.exception("can't add value of %s to text index for entity %s",
- rschema, entity.eid)
- continue
- if value:
- words.setdefault(weight, []).extend(tokenize(value))
- for rschema, role in entity.e_schema.fulltext_relations():
- if role == 'subject':
- for entity_ in getattr(entity, rschema.type):
- merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
- else: # if role == 'object':
- for entity_ in getattr(entity, 'reverse_%s' % rschema.type):
- merge_weight_dict(words, entity_.cw_adapt_to('IFTIndexable').get_words())
- return words
-
-
-def merge_weight_dict(maindict, newdict):
- for weight, words in newdict.items():
- maindict.setdefault(weight, []).extend(words)
-
-
-class IDownloadableAdapter(view.EntityAdapter):
- """interface for downloadable entities"""
- __regid__ = 'IDownloadable'
- __abstract__ = True
-
- def download_url(self, **kwargs): # XXX not really part of this interface
- """return a URL to download entity's content
-
- It should be a unicode object containing url-encoded ASCII.
- """
- raise NotImplementedError
-
- def download_content_type(self):
- """return MIME type (unicode) of the downloadable content"""
- raise NotImplementedError
-
- def download_encoding(self):
- """return encoding (unicode) of the downloadable content"""
- raise NotImplementedError
-
- def download_file_name(self):
- """return file name (unicode) of the downloadable content"""
- raise NotImplementedError
-
- def download_data(self):
- """return actual data (bytes) of the downloadable content"""
- raise NotImplementedError
-
-
-# XXX should propose to use two different relations for children/parent
-class ITreeAdapter(view.EntityAdapter):
- """This adapter provides a tree interface.
-
- It has to be overriden to be configured using the tree_relation,
- child_role and parent_role class attributes to benefit from this default
- implementation.
-
- This class provides the following methods:
-
- .. automethod: iterparents
- .. automethod: iterchildren
- .. automethod: prefixiter
-
- .. automethod: is_leaf
- .. automethod: is_root
-
- .. automethod: root
- .. automethod: parent
- .. automethod: children
- .. automethod: different_type_children
- .. automethod: same_type_children
- .. automethod: children_rql
- .. automethod: path
- """
- __regid__ = 'ITree'
- __abstract__ = True
-
- child_role = 'subject'
- parent_role = 'object'
-
- def children_rql(self):
- """Returns RQL to get the children of the entity."""
- return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
-
- def different_type_children(self, entities=True):
- """Return children entities of different type as this entity.
-
- According to the `entities` parameter, return entity objects or the
- equivalent result set.
- """
- res = self.entity.related(self.tree_relation, self.parent_role,
- entities=entities)
- eschema = self.entity.e_schema
- if entities:
- return [e for e in res if e.e_schema != eschema]
- return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
-
- def same_type_children(self, entities=True):
- """Return children entities of the same type as this entity.
-
- According to the `entities` parameter, return entity objects or the
- equivalent result set.
- """
- res = self.entity.related(self.tree_relation, self.parent_role,
- entities=entities)
- eschema = self.entity.e_schema
- if entities:
- return [e for e in res if e.e_schema == eschema]
- return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
-
- def is_leaf(self):
- """Returns True if the entity does not have any children."""
- return len(self.children()) == 0
-
- def is_root(self):
- """Returns true if the entity is root of the tree (e.g. has no parent).
- """
- return self.parent() is None
-
- def root(self):
- """Return the root entity of the tree."""
- return self._cw.entity_from_eid(self.path()[0])
-
- def parent(self):
- """Returns the parent entity if any, else None (e.g. if we are on the
- root).
- """
- try:
- return self.entity.related(self.tree_relation, self.child_role,
- entities=True)[0]
- except (KeyError, IndexError):
- return None
-
- def children(self, entities=True, sametype=False):
- """Return children entities.
-
- According to the `entities` parameter, return entity objects or the
- equivalent result set.
- """
- if sametype:
- return self.same_type_children(entities)
- else:
- return self.entity.related(self.tree_relation, self.parent_role,
- entities=entities)
-
- def iterparents(self, strict=True):
- """Return an iterator on the parents of the entity."""
- def _uptoroot(self):
- curr = self
- while True:
- curr = curr.parent()
- if curr is None:
- break
- yield curr
- curr = curr.cw_adapt_to('ITree')
- if not strict:
- return chain([self.entity], _uptoroot(self))
- return _uptoroot(self)
-
- def iterchildren(self, _done=None):
- """Return an iterator over the item's children."""
- if _done is None:
- _done = set()
- for child in self.children():
- if child.eid in _done:
- self.error('loop in %s tree: %s', child.cw_etype.lower(), child)
- continue
- yield child
- _done.add(child.eid)
-
- def prefixiter(self, _done=None):
- """Return an iterator over the item's descendants in a prefixed order."""
- if _done is None:
- _done = set()
- if self.entity.eid in _done:
- return
- _done.add(self.entity.eid)
- yield self.entity
- for child in self.same_type_children():
- for entity in child.cw_adapt_to('ITree').prefixiter(_done):
- yield entity
-
- @cached
- def path(self):
- """Returns the list of eids from the root object to this object."""
- path = []
- adapter = self
- entity = adapter.entity
- while entity is not None:
- if entity.eid in path:
- self.error('loop in %s tree: %s', entity.cw_etype.lower(), entity)
- break
- path.append(entity.eid)
- try:
- # check we are not jumping to another tree
- if (adapter.tree_relation != self.tree_relation or
- adapter.child_role != self.child_role):
- break
- entity = adapter.parent()
- adapter = entity.cw_adapt_to('ITree')
- except AttributeError:
- break
- path.reverse()
- return path
-
-
-class ISerializableAdapter(view.EntityAdapter):
- """Adapter to serialize an entity to a bare python structure that may be
- directly serialized to e.g. JSON.
- """
-
- __regid__ = 'ISerializable'
- __select__ = is_instance('Any')
-
- def serialize(self):
- entity = self.entity
- entity.complete()
- data = {
- 'cw_etype': entity.cw_etype,
- 'cw_source': entity.cw_metainformation()['source']['uri'],
- 'eid': entity.eid,
- }
- for rschema, __ in entity.e_schema.attribute_definitions():
- attr = rschema.type
- try:
- value = entity.cw_attr_cache[attr]
- except KeyError:
- # Bytes
- continue
- data[attr] = value
- return data
-
-
-# error handling adapters ######################################################
-
-
-class IUserFriendlyError(view.EntityAdapter):
- __regid__ = 'IUserFriendlyError'
- __abstract__ = True
-
- def __init__(self, *args, **kwargs):
- self.exc = kwargs.pop('exc')
- super(IUserFriendlyError, self).__init__(*args, **kwargs)
-
-
-class IUserFriendlyUniqueTogether(IUserFriendlyError):
- __select__ = match_exception(UniqueTogetherError)
-
- def raise_user_exception(self):
- rtypes = self.exc.rtypes
- errors = {}
- msgargs = {}
- i18nvalues = []
- for rtype in rtypes:
- errors[rtype] = _('%(KEY-rtype)s is part of violated unicity constraint')
- msgargs[rtype + '-rtype'] = rtype
- i18nvalues.append(rtype + '-rtype')
- errors[''] = _('some relations violate a unicity constraint')
- raise ValidationError(self.entity.eid, errors, msgargs=msgargs, i18nvalues=i18nvalues)
-
-
-class IUserFriendlyCheckConstraint(IUserFriendlyError):
- __select__ = match_exception(ViolatedConstraint)
-
- def raise_user_exception(self):
- cstrname = self.exc.cstrname
- eschema = self.entity.e_schema
- for rschema, attrschema in eschema.attribute_definitions():
- rdef = rschema.rdef(eschema, attrschema)
- for constraint in rdef.constraints:
- if cstrname == 'cstr' + md5(
- (eschema.type + rschema.type + constraint.type() +
- (constraint.serialize() or '')).encode('ascii')).hexdigest():
- break
- else:
- continue
- break
- else:
- assert 0
- key = rschema.type + '-subject'
- msg, args = constraint.failed_message(key, self.entity.cw_edited[rschema.type])
- raise ValidationError(self.entity.eid, {key: msg}, args)