diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/entities/adapters.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/entities/adapters.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,427 @@ +# 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 . +"""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)