# copyright 2010-2012 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 theframework itself."""__docformat__="restructuredtext en"fromitertoolsimportchainfromwarningsimportwarnfromlogilab.mtconverterimportTransformErrorfromlogilab.common.decoratorsimportcachedfromcubicwebimportValidationError,viewfromcubicweb.predicatesimport(implements,is_instance,relation_possible,match_exception)fromcubicweb.interfacesimportIDownloadable,ITreeclassIEmailableAdapter(view.EntityAdapter):__regid__='IEmailable'__select__=relation_possible('primary_email')|relation_possible('use_email')defget_email(self):ifgetattr(self.entity,'primary_email',None):returnself.entity.primary_email[0].addressifgetattr(self.entity,'use_email',None):returnself.entity.use_email[0].addressreturnNonedefallowed_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. """returnset(rschema.typeforrschema,attrtypeinself.entity.e_schema.attribute_definitions()ifattrtype.typenotin('Password','Bytes'))defas_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. """returndict((attr,getattr(self.entity,attr))forattrinself.allowed_massmail_keys())classINotifiableAdapter(view.EntityAdapter):__needs_bw_compat__=True__regid__='INotifiable'__select__=is_instance('Any')@view.implements_adapter_compat('INotifiableAdapter')defnotification_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')ifitreeisnotNone:returnitree.path()[:-1]return()classIFTIndexableAdapter(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')deffti_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_doneisNone:_done=set()entity=self.entity_done.add(entity.eid)containers=tuple(entity.e_schema.fulltext_containers())ifcontainers:forrschema,targetincontainers:iftarget=='object':targets=getattr(entity,rschema.type)else:targets=getattr(entity,'reverse_%s'%rschema)forentityintargets:ifentity.eidin_done:continueforcontainerinentity.cw_adapt_to('IFTIndexable').fti_containers(_done):yieldcontaineryielded=Trueelse:yieldentity# weight in ABCDentity_weight=1.0attr_weight={}defget_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 """fromlogilab.database.ftiimporttokenize# take care to cases where we're modyfying the schemaentity=self.entitypending=self._cw.transaction_data.setdefault('pendingrdefs',set())words={}forrschemainentity.e_schema.indexable_attributes():if(entity.e_schema,rschema)inpending:continueweight=self.attr_weight.get(rschema,'C')try:value=entity.printable_value(rschema,format='text/plain')exceptTransformError:continueexceptException:self.exception("can't add value of %s to text index for entity %s",rschema,entity.eid)continueifvalue:words.setdefault(weight,[]).extend(tokenize(value))forrschema,roleinentity.e_schema.fulltext_relations():ifrole=='subject':forentity_ingetattr(entity,rschema.type):merge_weight_dict(words,entity_.cw_adapt_to('IFTIndexable').get_words())else:# if role == 'object':forentity_ingetattr(entity,'reverse_%s'%rschema.type):merge_weight_dict(words,entity_.cw_adapt_to('IFTIndexable').get_words())returnwordsdefmerge_weight_dict(maindict,newdict):forweight,wordsinnewdict.iteritems():maindict.setdefault(weight,[]).extend(words)classIDownloadableAdapter(view.EntityAdapter):"""interface for downloadable entities"""__needs_bw_compat__=True__regid__='IDownloadable'__select__=implements(IDownloadable,warn=False)# XXX for bw compat, else should be abstract@view.implements_adapter_compat('IDownloadable')defdownload_url(self,**kwargs):# XXX not really part of this interface"""return an url to download entity's content"""raiseNotImplementedError@view.implements_adapter_compat('IDownloadable')defdownload_content_type(self):"""return MIME type of the downloadable content"""raiseNotImplementedError@view.implements_adapter_compat('IDownloadable')defdownload_encoding(self):"""return encoding of the downloadable content"""raiseNotImplementedError@view.implements_adapter_compat('IDownloadable')defdownload_file_name(self):"""return file name of the downloadable content"""raiseNotImplementedError@view.implements_adapter_compat('IDownloadable')defdownload_data(self):"""return actual data of the downloadable content"""raiseNotImplementedError# XXX should propose to use two different relations for children/parentclassITreeAdapter(view.EntityAdapter):"""This adapter 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 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 """__needs_bw_compat__=True__regid__='ITree'__select__=implements(ITree,warn=False)# XXX for bw compat, else should be abstractchild_role='subject'parent_role='object'@propertydeftree_relation(self):warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom ''ITree for %s instead'%(self.entity.__class__),DeprecationWarning)returnself.entity.tree_attribute# XXX should be removed from the public interface@view.implements_adapter_compat('ITree')defchildren_rql(self):"""Returns RQL to get the children of the entity."""returnself.entity.cw_related_rql(self.tree_relation,self.parent_role)@view.implements_adapter_compat('ITree')defdifferent_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_schemaifentities:return[eforeinresife.e_schema!=eschema]returnres.filtered_rset(lambdax:x.e_schema!=eschema,self.entity.cw_col)@view.implements_adapter_compat('ITree')defsame_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_schemaifentities:return[eforeinresife.e_schema==eschema]returnres.filtered_rset(lambdax:x.e_schemaiseschema,self.entity.cw_col)@view.implements_adapter_compat('ITree')defis_leaf(self):"""Returns True if the entity does not have any children."""returnlen(self.children())==0@view.implements_adapter_compat('ITree')defis_root(self):"""Returns true if the entity is root of the tree (e.g. has no parent). """returnself.parent()isNone@view.implements_adapter_compat('ITree')defroot(self):"""Return the root entity of the tree."""returnself._cw.entity_from_eid(self.path()[0])@view.implements_adapter_compat('ITree')defparent(self):"""Returns the parent entity if any, else None (e.g. if we are on the root). """try:returnself.entity.related(self.tree_relation,self.child_role,entities=True)[0]except(KeyError,IndexError):returnNone@view.implements_adapter_compat('ITree')defchildren(self,entities=True,sametype=False):"""Return children entities. According to the `entities` parameter, return entity objects or the equivalent result set. """ifsametype:returnself.same_type_children(entities)else:returnself.entity.related(self.tree_relation,self.parent_role,entities=entities)@view.implements_adapter_compat('ITree')defiterparents(self,strict=True):"""Return an iterator on the parents of the entity."""def_uptoroot(self):curr=selfwhileTrue:curr=curr.parent()ifcurrisNone:breakyieldcurrcurr=curr.cw_adapt_to('ITree')ifnotstrict:returnchain([self.entity],_uptoroot(self))return_uptoroot(self)@view.implements_adapter_compat('ITree')defiterchildren(self,_done=None):"""Return an iterator over the item's children."""if_doneisNone:_done=set()forchildinself.children():ifchild.eidin_done:self.error('loop in %s tree: %s',child.cw_etype.lower(),child)continueyieldchild_done.add(child.eid)@view.implements_adapter_compat('ITree')defprefixiter(self,_done=None):"""Return an iterator over the item's descendants in a prefixed order."""if_doneisNone:_done=set()ifself.entity.eidin_done:return_done.add(self.entity.eid)yieldself.entityforchildinself.same_type_children():forentityinchild.cw_adapt_to('ITree').prefixiter(_done):yieldentity@view.implements_adapter_compat('ITree')@cacheddefpath(self):"""Returns the list of eids from the root object to this object."""path=[]adapter=selfentity=adapter.entitywhileentityisnotNone:ifentity.eidinpath:self.error('loop in %s tree: %s',entity.cw_etype.lower(),entity)breakpath.append(entity.eid)try:# check we are not jumping to another treeif(adapter.tree_relation!=self.tree_relationoradapter.child_role!=self.child_role):breakentity=adapter.parent()adapter=entity.cw_adapt_to('ITree')exceptAttributeError:breakpath.reverse()returnpath# error handling adapters ######################################################fromcubicwebimportUniqueTogetherErrorclassIUserFriendlyError(view.EntityAdapter):__regid__='IUserFriendlyError'__abstract__=Truedef__init__(self,*args,**kwargs):self.exc=kwargs.pop('exc')super(IUserFriendlyError,self).__init__(*args,**kwargs)classIUserFriendlyUniqueTogether(IUserFriendlyError):__select__=match_exception(UniqueTogetherError)defraise_user_exception(self):etype,rtypes=self.exc.argsmsg=self._cw._('violates unique_together constraints (%s)')%(', '.join([self._cw._(rtype)forrtypeinrtypes]))raiseValidationError(self.entity.eid,dict((col,msg)forcolinrtypes))# deprecated ###################################################################classadapter_deprecated(view.auto_unwrap_bw_compat):"""metaclass to print a warning on instantiation of a deprecated class"""def__call__(cls,*args,**kwargs):msg=getattr(cls,"__deprecation_warning__","%(cls)s is deprecated")%{'cls':cls.__name__}warn(msg,DeprecationWarning,stacklevel=2)returntype.__call__(cls,*args,**kwargs)