cubicweb/entities/adapters.py
changeset 11057 0b59724cb3f2
parent 11044 00c5ee272a6d
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     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)