"""Base class for entity objects manipulated in clients:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"fromlogilab.commonimportinterfacefromlogilab.common.compatimportallfromlogilab.common.decoratorsimportcachedfromlogilab.mtconverterimportTransformData,TransformErrorfromrql.utilsimportrqlvar_makerfromcubicwebimportUnauthorizedfromcubicweb.vregistryimportautoselectorsfromcubicweb.rsetimportResultSetfromcubicweb.common.appobjectimportAppRsetObjectfromcubicweb.common.registerersimportid_registererfromcubicweb.common.selectorsimportyes_selectorfromcubicweb.common.uilibimportprintable_value,html_escape,soup2xhtmlfromcubicweb.common.mixinsimportMI_REL_TRIGGERSfromcubicweb.common.mttransformsimportENGINEfromcubicweb.schemaimportRQLVocabularyConstraint,RQLConstraint,bw_normalize_etype_marker=object()defgreater_card(rschema,subjtypes,objtypes,index):forsubjtypeinsubjtypes:forobjtypeinobjtypes:card=rschema.rproperty(subjtype,objtype,'cardinality')[index]ifcardin'+*':returncardreturn'1'classRelationTags(object):MODE_TAGS=frozenset(('link','create'))CATEGORY_TAGS=frozenset(('primary','secondary','generic','generated','inlineview'))def__init__(self,eclass,tagdefs):self.eclass=eclassself._tagdefs={}forrelation,tagsintagdefs.iteritems():# tags must become a setifisinstance(tags,basestring):tags=set((tags,))elifnotisinstance(tags,set):tags=set(tags)# relation must become a 3-uple (rtype, targettype, role)ifisinstance(relation,basestring):self._tagdefs[(relation,'*','subject')]=tagsself._tagdefs[(relation,'*','object')]=tagseliflen(relation)==1:# useful ?self._tagdefs[(relation[0],'*','subject')]=tagsself._tagdefs[(relation[0],'*','object')]=tagseliflen(relation)==2:rtype,ttype=relationttype=bw_normalize_etype(ttype)# XXX bw compatself._tagdefs[rtype,ttype,'subject']=tagsself._tagdefs[rtype,ttype,'object']=tagseliflen(relation)==3:relation=list(relation)# XXX bw compatrelation[1]=bw_normalize_etype(relation[1])self._tagdefs[tuple(relation)]=tagselse:raiseValueError('bad rtag definition (%r)'%(relation,))def__initialize__(self):# eclass.[*]schema are only set when registeringself.schema=self.eclass.schemaeschema=self.eschema=self.eclass.e_schemartags=self._tagdefs# expand wildcards in rtags and add automatic tagsforrschema,tschemas,roleinsorted(eschema.relation_definitions(True)):rtype=rschema.typestar_tags=rtags.pop((rtype,'*',role),set())fortschemaintschemas:tags=rtags.setdefault((rtype,tschema.type,role),set(star_tags))ifrole=='subject':X,Y=eschema,tschemacard=rschema.rproperty(X,Y,'cardinality')[0]composed=rschema.rproperty(X,Y,'composite')=='object'else:X,Y=tschema,eschemacard=rschema.rproperty(X,Y,'cardinality')[1]composed=rschema.rproperty(X,Y,'composite')=='subject'# set default category tags if neededifnottags&self.CATEGORY_TAGS:ifcardin'1+':ifnotrschema.is_final()andcomposed:category='generated'elifrschema.is_final()and(rschema.type.endswith('_format')orrschema.type.endswith('_encoding')):category='generated'else:category='primary'elifrschema.is_final():if(rschema.type.endswith('_format')orrschema.type.endswith('_encoding')):category='generated'else:category='secondary'else:category='generic'tags.add(category)ifnottags&self.MODE_TAGS:ifcardin'?1':# by default, suppose link mode if cardinality doesn't allow# more than one relationmode='link'elifrschema.rproperty(X,Y,'composite')==role:# if self is composed of the target type, create modemode='create'else:# link mode by defaultmode='link'tags.add(mode)def_default_target(self,rschema,role='subject'):eschema=self.eschemaifrole=='subject':returneschema.subject_relation(rschema).objects(eschema)[0]else:returneschema.object_relation(rschema).subjects(eschema)[0]# dict compatdef__getitem__(self,key):ifisinstance(key,basestring):key=(key,)returnself.get_tags(*key)__contains__=__getitem__defget_tags(self,rtype,targettype=None,role='subject'):rschema=self.schema.rschema(rtype)iftargettypeisNone:tschema=self._default_target(rschema,role)else:tschema=self.schema.eschema(targettype)returnself._tagdefs[(rtype,tschema.type,role)]__call__=get_tagsdefget_mode(self,rtype,targettype=None,role='subject'):# XXX: should we make an assertion on rtype not being final ?# assert not rschema.is_final()tags=self.get_tags(rtype,targettype,role)# do not change the intersection order !modes=tags&self.MODE_TAGSassertlen(modes)==1returnmodes.pop()defget_category(self,rtype,targettype=None,role='subject'):tags=self.get_tags(rtype,targettype,role)categories=tags&self.CATEGORY_TAGSassertlen(categories)==1returncategories.pop()defis_inlined(self,rtype,targettype=None,role='subject'):# return set(('primary', 'secondary')) & self.get_tags(rtype, targettype)return'inlineview'inself.get_tags(rtype,targettype,role)classmetaentity(autoselectors):"""this metaclass sets the relation tags on the entity class and deals with the `widgets` attribute """def__new__(mcs,name,bases,classdict):# collect baseclass' rtagstagdefs={}widgets={}forbaseinbases:tagdefs.update(getattr(base,'__rtags__',{}))widgets.update(getattr(base,'widgets',{}))# update with the class' own rtgastagdefs.update(classdict.get('__rtags__',{}))widgets.update(classdict.get('widgets',{}))# XXX decide whether or not it's a good idea to replace __rtags__# good point: transparent support for inheritance levels >= 2# bad point: we loose the information of which tags are specific# to this entity classclassdict['__rtags__']=tagdefsclassdict['widgets']=widgetseclass=super(metaentity,mcs).__new__(mcs,name,bases,classdict)# adds the "rtags" attributeeclass.rtags=RelationTags(eclass,tagdefs)returneclassclassEntity(AppRsetObject,dict):"""an entity instance has e_schema automagically set on the class and instances has access to their issuing cursor. A property is set for each attribute and relation on each entity's type class. Becare that among attributes, 'eid' is *NEITHER* stored in the dict containment (which acts as a cache for other attributes dynamically fetched) :type e_schema: `cubicweb.schema.EntitySchema` :ivar e_schema: the entity's schema :type rest_var: str :cvar rest_var: indicates which attribute should be used to build REST urls If None is specified, the first non-meta attribute will be used :type skip_copy_for: list :cvar skip_copy_for: a list of relations that should be skipped when copying this kind of entity. Note that some relations such as composite relations or relations that have '?1' as object cardinality """__metaclass__=metaentity__registry__='etypes'__registerer__=id_registerer__selectors__=(yes_selector,)widgets={}id=Nonee_schema=Noneeid=Nonerest_attr=Noneskip_copy_for=()@classmethoddefregistered(cls,registry):"""build class using descriptor at registration time"""assertcls.idisnotNonesuper(Entity,cls).registered(registry)ifcls.id!='Any':cls.__initialize__()returnclsMODE_TAGS=set(('link','create'))CATEGORY_TAGS=set(('primary','secondary','generic','generated'))# , 'metadata'))@classmethoddef__initialize__(cls):"""initialize a specific entity class by adding descriptors to access entity type's attributes and relations """etype=cls.idassertetype!='Any',etypecls.e_schema=eschema=cls.schema.eschema(etype)forrschema,_ineschema.attribute_definitions():ifrschema.type=='eid':continuesetattr(cls,rschema.type,Attribute(rschema.type))mixins=[]forrschema,_,xineschema.relation_definitions():if(rschema,x)inMI_REL_TRIGGERS:mixin=MI_REL_TRIGGERS[(rschema,x)]ifnot(issubclass(cls,mixin)ormixininmixins):# already mixed ?mixins.append(mixin)forifaceingetattr(mixin,'__implements__',()):ifnotinterface.implements(cls,iface):interface.extend(cls,iface)ifx=='subject':setattr(cls,rschema.type,SubjectRelation(rschema))else:attr='reverse_%s'%rschema.typesetattr(cls,attr,ObjectRelation(rschema))ifmixins:cls.__bases__=tuple(mixins+[pforpincls.__bases__ifnotpisobject])cls.debug('plugged %s mixins on %s',mixins,etype)cls.rtags.__initialize__()@classmethoddeffetch_rql(cls,user,restriction=None,fetchattrs=None,mainvar='X',settype=True,ordermethod='fetch_order'):"""return a rql to fetch all entities of the class type"""restrictions=restrictionor[]ifsettype:restrictions.append('%s is %s'%(mainvar,cls.id))iffetchattrsisNone:fetchattrs=cls.fetch_attrsselection=[mainvar]orderby=[]# start from 26 to avoid possible conflicts with Xvarmaker=rqlvar_maker(index=26)cls._fetch_restrictions(mainvar,varmaker,fetchattrs,selection,orderby,restrictions,user,ordermethod)rql='Any %s'%','.join(selection)iforderby:rql+=' ORDERBY %s'%','.join(orderby)rql+=' WHERE %s'%', '.join(restrictions)returnrql@classmethoddef_fetch_restrictions(cls,mainvar,varmaker,fetchattrs,selection,orderby,restrictions,user,ordermethod='fetch_order',visited=None):eschema=cls.e_schemaifvisitedisNone:visited=set((eschema.type,))elifeschema.typeinvisited:# avoid infinite recursionreturnelse:visited.add(eschema.type)_fetchattrs=[]forattrinfetchattrs:try:rschema=eschema.subject_relation(attr)exceptKeyError:cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',attr,cls.id)continueifnotuser.matching_groups(rschema.get_groups('read')):continuevar=varmaker.next()selection.append(var)restriction='%s%s%s'%(mainvar,attr,var)restrictions.append(restriction)ifnotrschema.is_final():# XXX this does not handle several destination typesdesttype=rschema.objects(eschema.type)[0]card=rschema.rproperty(eschema,desttype,'cardinality')[0]ifcardnotin'?1':selection.pop()restrictions.pop()continueifcard=='?':restrictions[-1]+='?'# left outer join if not mandatorydestcls=cls.vreg.etype_class(desttype)destcls._fetch_restrictions(var,varmaker,destcls.fetch_attrs,selection,orderby,restrictions,user,ordermethod,visited=visited)orderterm=getattr(cls,ordermethod)(attr,var)iforderterm:orderby.append(orderterm)returnselection,orderby,restrictionsdef__init__(self,req,rset,row=None,col=0):AppRsetObject.__init__(self,req,rset)dict.__init__(self)self.row,self.col=row,colself._related_cache={}ifrsetisnotNone:self.eid=rset[row][col]else:self.eid=Noneself._is_saved=Truedef__repr__(self):return'<Entity %s%s%s at %s>'%(self.e_schema,self.eid,self.keys(),id(self))def__nonzero__(self):returnTruedef__hash__(self):returnid(self)defpre_add_hook(self):"""hook called by the repository before doing anything to add the entity (before_add entity hooks have not been called yet). This give the occasion to do weird stuff such as autocast (File -> Image for instance). This method must return the actual entity to be added. """returnselfdefset_eid(self,eid):self.eid=self['eid']=eiddefhas_eid(self):"""return True if the entity has an attributed eid (False meaning that the entity has to be created """try:int(self.eid)returnTrueexcept(ValueError,TypeError):returnFalsedefis_saved(self):"""during entity creation, there is some time during which the entity has an eid attributed though it's not saved (eg during before_add_entity hooks). You can use this method to ensure the entity has an eid *and* is saved in its source. """returnself.has_eid()andself._is_saved@cacheddefmetainformation(self):res=dict(zip(('type','source','extid'),self.req.describe(self.eid)))res['source']=self.req.source_defs()[res['source']]returnresdefcheck_perm(self,action):self.e_schema.check_perm(self.req,action,self.eid)defhas_perm(self,action):returnself.e_schema.has_perm(self.req,action,self.eid)defview(self,vid,__registry='views',**kwargs):"""shortcut to apply a view on this entity"""returnself.vreg.render(__registry,vid,self.req,rset=self.rset,row=self.row,col=self.col,**kwargs)defabsolute_url(self,method=None,**kwargs):"""return an absolute url to view this entity"""# in linksearch mode, we don't want external urls else selecting# the object for use in the relation is tricky# XXX search_state is web specificifgetattr(self.req,'search_state',('normal',))[0]=='normal':kwargs['base_url']=self.metainformation()['source'].get('base-url')ifmethodisNoneormethod=='view':kwargs['_restpath']=self.rest_path()else:kwargs['rql']='Any X WHERE X eid %s'%self.eidreturnself.build_url(method,**kwargs)defrest_path(self):"""returns a REST-like (relative) path for this entity"""mainattr,needcheck=self._rest_attr_info()etype=str(self.e_schema)ifmainattr=='eid':value=self.eidelse:value=getattr(self,mainattr)ifvalueisNone:return'%s/eid/%s'%(etype.lower(),self.eid)ifneedcheck:# make sure url is not ambiguousrql='Any COUNT(X) WHERE X is %s, X %s%%(value)s'%(etype,mainattr)ifvalueisnotNone:nbresults=self.req.execute(rql,{'value':value})[0][0]# may an assertion that nbresults is not 0 would be a good ideaifnbresults!=1:# no ambiguityreturn'%s/eid/%s'%(etype.lower(),self.eid)return'%s/%s'%(etype.lower(),self.req.url_quote(value))@classmethoddef_rest_attr_info(cls):mainattr,needcheck='eid',Trueifcls.rest_attr:mainattr=cls.rest_attrneedcheck=notcls.e_schema.has_unique_values(mainattr)else:forrschemaincls.e_schema.subject_relations():ifrschema.is_final()andrschema!='eid'andcls.e_schema.has_unique_values(rschema):mainattr=str(rschema)needcheck=Falsebreakifmainattr=='eid':needcheck=Falsereturnmainattr,needcheck@cacheddefformatted_attrs(self):"""returns the list of attributes which have some format information (i.e. rich text strings) """attrs=[]forrschema,attrschemainself.e_schema.attribute_definitions():ifattrschema.type=='String'andself.has_format(rschema):attrs.append(rschema.type)returnattrsdefformat(self,attr):"""return the mime type format for an attribute (if specified)"""returngetattr(self,'%s_format'%attr,None)deftext_encoding(self,attr):"""return the text encoding for an attribute, default to site encoding """encoding=getattr(self,'%s_encoding'%attr,None)returnencodingorself.vreg.property_value('ui.encoding')defhas_format(self,attr):"""return true if this entity's schema has a format field for the given attribute """returnself.e_schema.has_subject_relation('%s_format'%attr)defhas_text_encoding(self,attr):"""return true if this entity's schema has ab encoding field for the given attribute """returnself.e_schema.has_subject_relation('%s_encoding'%attr)defprintable_value(self,attr,value=_marker,attrtype=None,format='text/html',displaytime=True):"""return a displayable value (i.e. unicode string) which may contains html tags """attr=str(attr)ifvalueis_marker:value=getattr(self,attr)ifisinstance(value,basestring):value=value.strip()ifvalueisNoneorvalue=='':# don't use "not", 0 is an acceptable valuereturnu''ifattrtypeisNone:attrtype=self.e_schema.destination(attr)props=self.e_schema.rproperties(attr)ifattrtype=='String':# internalinalized *and* formatted string such as schema# description...ifprops.get('internationalizable'):value=self.req._(value)attrformat=self.format(attr)ifattrformat:returnself.mtc_transform(value,attrformat,format,self.req.encoding)elifattrtype=='Bytes':attrformat=self.format(attr)ifattrformat:try:encoding=getattr(self,'%s_encoding'%attr)exceptAttributeError:encoding=self.req.encodingreturnself.mtc_transform(value.getvalue(),attrformat,format,encoding)returnu''value=printable_value(self.req,attrtype,value,props,displaytime)ifformat=='text/html':value=html_escape(value)returnvaluedefmtc_transform(self,data,format,target_format,encoding,_engine=ENGINE):trdata=TransformData(data,format,encoding,appobject=self)data=_engine.convert(trdata,target_format).decode()ifformat=='text/html':data=soup2xhtml(data,self.req.encoding)returndata# entity cloning ##########################################################defcopy_relations(self,ceid):"""copy relations of the object with the given eid on this object By default meta and composite relations are skipped. Overrides this if you want another behaviour """assertself.has_eid()execute=self.req.executeforrschemainself.e_schema.subject_relations():ifrschema.metaorrschema.is_final():continue# skip already defined relationsifgetattr(self,rschema.type):continueifrschema.typeinself.skip_copy_for:continueifrschema.type=='in_state':# if the workflow is defining an initial state (XXX AND we are# not in the managers group? not done to be more consistent)# don't try to copy in_stateifexecute('Any S WHERE S state_of ET, ET initial_state S,''ET name %(etype)s',{'etype':str(self.e_schema)}):continue# skip composite relationifself.e_schema.subjrproperty(rschema,'composite'):continue# skip relation with card in ?1 else we either change the copied# object (inlined relation) or inserting some inconsistencyifself.e_schema.subjrproperty(rschema,'cardinality')[1]in'?1':continuerql='SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V'%(rschema.type,rschema.type)execute(rql,{'x':self.eid,'y':ceid},('x','y'))self.clear_related_cache(rschema.type,'subject')forrschemainself.e_schema.object_relations():ifrschema.meta:continue# skip already defined relationsifgetattr(self,'reverse_%s'%rschema.type):continue# skip composite relationifself.e_schema.objrproperty(rschema,'composite'):continue# skip relation with card in ?1 else we either change the copied# object (inlined relation) or inserting some inconsistencyifself.e_schema.objrproperty(rschema,'cardinality')[0]in'?1':continuerql='SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y'%(rschema.type,rschema.type)execute(rql,{'x':self.eid,'y':ceid},('x','y'))self.clear_related_cache(rschema.type,'object')# data fetching methods ###################################################@cacheddefas_rset(self):"""returns a resultset containing `self` information"""rset=ResultSet([(self.eid,)],'Any X WHERE X eid %(x)s',{'x':self.eid},[(self.id,)])returnself.req.decorate_rset(rset)defto_complete_relations(self):"""by default complete final relations to when calling .complete()"""forrschemainself.e_schema.subject_relations():ifrschema.is_final():continueiflen(rschema.objects(self.e_schema))>1:# ambigous relations, the querier doesn't handle# outer join correctly in this casecontinueifrschema.inlined:matching_groups=self.req.user.matching_groupsifmatching_groups(rschema.get_groups('read'))and \all(matching_groups(es.get_groups('read'))foresinrschema.objects(self.e_schema)):yieldrschema,'subject'defto_complete_attributes(self,skip_bytes=True):forrschema,attrschemainself.e_schema.attribute_definitions():# skip binary data by defaultifskip_bytesandattrschema.type=='Bytes':continueattr=rschema.typeifattr=='eid':continue# password retreival is blocked at the repository server levelifnotself.req.user.matching_groups(rschema.get_groups('read')) \orattrschema.type=='Password':self[attr]=Nonecontinueyieldattrdefcomplete(self,attributes=None,skip_bytes=True):"""complete this entity by adding missing attributes (i.e. query the repository to fill the entity) :type skip_bytes: bool :param skip_bytes: if true, attribute of type Bytes won't be considered """assertself.has_eid()varmaker=rqlvar_maker()V=varmaker.next()rql=['WHERE %s eid %%(x)s'%V]selected=[]forattrin(attributesorself.to_complete_attributes(skip_bytes)):# if attribute already in entity, nothing to doifself.has_key(attr):continue# case where attribute must be completed, but is not yet in entityvar=varmaker.next()rql.append('%s%s%s'%(V,attr,var))selected.append((attr,var))# +1 since this doen't include the main variablelastattr=len(selected)+1ifattributesisNone:# fetch additional relations (restricted to 0..1 relations)forrschema,roleinself.to_complete_relations():rtype=rschema.typeifself.relation_cached(rtype,role):continuevar=varmaker.next()ifrole=='subject':targettype=rschema.objects(self.e_schema)[0]card=rschema.rproperty(self.e_schema,targettype,'cardinality')[0]ifcard=='1':rql.append('%s%s%s'%(V,rtype,var))else:# '?"rql.append('%s%s%s?'%(V,rtype,var))else:targettype=rschema.subjects(self.e_schema)[1]card=rschema.rproperty(self.e_schema,targettype,'cardinality')[1]ifcard=='1':rql.append('%s%s%s'%(var,rtype,V))else:# '?"rql.append('%s? %s%s'%(var,rtype,V))assertcardin'1?','%s%s%s%s'%(self.e_schema,rtype,role,card)selected.append(((rtype,role),var))ifselected:# select V, we need it as the left most selected variable# if some outer join are included to fetch inlined relationsrql='Any %s,%s%s'%(V,','.join(varforattr,varinselected),','.join(rql))execute=getattr(self.req,'unsafe_execute',self.req.execute)rset=execute(rql,{'x':self.eid},'x',build_descr=False)[0]# handle attributesforiinxrange(1,lastattr):self[str(selected[i-1][0])]=rset[i]# handle relationsforiinxrange(lastattr,len(rset)):rtype,x=selected[i-1][0]value=rset[i]ifvalueisNone:rrset=ResultSet([],rql,{'x':self.eid})self.req.decorate_rset(rrset)else:rrset=self.req.eid_rset(value)self.set_related_cache(rtype,x,rrset)defget_value(self,name):"""get value for the attribute relation <name>, query the repository to get the value if necessary. :type name: str :param name: name of the attribute to get """try:value=self[name]exceptKeyError:ifnotself.is_saved():returnNonerql="Any A WHERE X eid %%(x)s, X %s A"%name# XXX should we really use unsafe_execute here??execute=getattr(self.req,'unsafe_execute',self.req.execute)try:rset=execute(rql,{'x':self.eid},'x')exceptUnauthorized:self[name]=value=Noneelse:assertrset.rowcount<=1,(self,rql,rset.rowcount)try:self[name]=value=rset.rows[0][0]exceptIndexError:# probably a multisource errorself.critical("can't get value for attribute %s of entity with eid %s",name,self.eid)ifself.e_schema.destination(name)=='String':self[name]=value=self.req._('unaccessible')else:self[name]=value=Nonereturnvaluedefrelated(self,rtype,role='subject',limit=None,entities=False):"""returns a resultset of related entities :param role: is the role played by 'self' in the relation ('subject' or 'object') :param limit: resultset's maximum size :param entities: if True, the entites are returned; if False, a result set is returned """try:returnself.related_cache(rtype,role,entities,limit)exceptKeyError:passassertself.has_eid()rql=self.related_rql(rtype,role)rset=self.req.execute(rql,{'x':self.eid},'x')self.set_related_cache(rtype,role,rset)returnself.related(rtype,role,limit,entities)defrelated_rql(self,rtype,role='subject'):rschema=self.schema[rtype]ifrole=='subject':targettypes=rschema.objects(self.e_schema)restriction='E eid %%(x)s, E %s X'%rtypecard=greater_card(rschema,(self.e_schema,),targettypes,0)else:targettypes=rschema.subjects(self.e_schema)restriction='E eid %%(x)s, X %s E'%rtypecard=greater_card(rschema,targettypes,(self.e_schema,),1)iflen(targettypes)>1:fetchattrs=set()forttypeintargettypes:etypecls=self.vreg.etype_class(ttype)fetchattrs&=frozenset(etypecls.fetch_attrs)rql=etypecls.fetch_rql(self.req.user,[restriction],fetchattrs,settype=False)else:etypecls=self.vreg.etype_class(targettypes[0])rql=etypecls.fetch_rql(self.req.user,[restriction],settype=False)# optimisation: remove ORDERBY if cardinality is 1 or ? (though# greater_card return 1 for those both cases)ifcard=='1':if' ORDERBY 'inrql:rql='%s WHERE %s'%(rql.split(' ORDERBY ',1)[0],rql.split(' WHERE ',1)[1])elifnot' ORDERBY 'inrql:args=tuple(rql.split(' WHERE ',1))rql='%s ORDERBY Z DESC WHERE X modification_date Z, %s'%argsreturnrql# generic vocabulary methods ##############################################defvocabulary(self,rtype,role='subject',limit=None):"""vocabulary functions must return a list of couples (label, eid) that will typically be used to fill the edition view's combobox. If `eid` is None in one of these couples, it should be interpreted as a separator in case vocabulary results are grouped """try:vocabfunc=getattr(self,'%s_%s_vocabulary'%(role,rtype))exceptAttributeError:vocabfunc=getattr(self,'%s_relation_vocabulary'%role)# NOTE: it is the responsibility of `vocabfunc` to sort the result# (direclty through RQL or via a python sort). This is also# important because `vocabfunc` might return a list with# couples (label, None) which act as separators. In these# cases, it doesn't make sense to sort results afterwards.returnvocabfunc(rtype,limit)defsubject_relation_vocabulary(self,rtype,limit=None):"""defaut vocabulary method for the given relation, looking for relation's object entities (i.e. self is the subject) """ifisinstance(rtype,basestring):rtype=self.schema.rschema(rtype)done=Noneassertnotrtype.is_final(),rtypeifself.has_eid():done=set(e.eidforeingetattr(self,str(rtype)))result=[]rsetsize=Noneforobjtypeinrtype.objects(self.e_schema):iflimitisnotNone:rsetsize=limit-len(result)result+=self.relation_vocabulary(rtype,objtype,'subject',rsetsize,done)iflimitisnotNoneandlen(result)>=limit:breakreturnresultdefobject_relation_vocabulary(self,rtype,limit=None):"""defaut vocabulary method for the given relation, looking for relation's subject entities (i.e. self is the object) """ifisinstance(rtype,basestring):rtype=self.schema.rschema(rtype)done=Noneifself.has_eid():done=set(e.eidforeingetattr(self,'reverse_%s'%rtype))result=[]rsetsize=Noneforsubjtypeinrtype.subjects(self.e_schema):iflimitisnotNone:rsetsize=limit-len(result)result+=self.relation_vocabulary(rtype,subjtype,'object',rsetsize,done)iflimitisnotNoneandlen(result)>=limit:breakreturnresultdefrelation_vocabulary(self,rtype,targettype,role,limit=None,done=None):ifdoneisNone:done=set()req=self.reqrset=self.unrelated(rtype,targettype,role,limit)res=[]forentityinrset.entities():ifentity.eidindone:continuedone.add(entity.eid)res.append((entity.view('combobox'),entity.eid))returnresdefunrelated_rql(self,rtype,targettype,role,ordermethod=None,vocabconstraints=True):"""build a rql to fetch `targettype` entities unrelated to this entity using (rtype, role) relation """ordermethod=ordermethodor'fetch_unrelated_order'ifisinstance(rtype,basestring):rtype=self.schema.rschema(rtype)ifrole=='subject':evar,searchedvar='S','O'subjtype,objtype=self.e_schema,targettypeelse:searchedvar,evar='S','O'objtype,subjtype=self.e_schema,targettypeifself.has_eid():restriction=['NOT S %s O'%rtype,'%s eid %%(x)s'%evar]else:restriction=[]constraints=rtype.rproperty(subjtype,objtype,'constraints')ifvocabconstraints:# RQLConstraint is a subclass for RQLVocabularyConstraint, so they# will be included as wellrestriction+=[cstr.restrictionforcstrinconstraintsifisinstance(cstr,RQLVocabularyConstraint)]else:restriction+=[cstr.restrictionforcstrinconstraintsifisinstance(cstr,RQLConstraint)]etypecls=self.vreg.etype_class(targettype)rql=etypecls.fetch_rql(self.req.user,restriction,mainvar=searchedvar,ordermethod=ordermethod)# ensure we have an order definedifnot' ORDERBY 'inrql:before,after=rql.split(' WHERE ',1)rql='%s ORDERBY %s WHERE %s'%(before,searchedvar,after)returnrqldefunrelated(self,rtype,targettype,role='subject',limit=None,ordermethod=None):"""return a result set of target type objects that may be related by a given relation, with self as subject or object """rql=self.unrelated_rql(rtype,targettype,role,ordermethod)iflimitisnotNone:before,after=rql.split(' WHERE ',1)rql='%s LIMIT %s WHERE %s'%(before,limit,after)ifself.has_eid():returnself.req.execute(rql,{'x':self.eid})returnself.req.execute(rql)# relations cache handling ################################################defrelation_cached(self,rtype,role):"""return true if the given relation is already cached on the instance """return'%s_%s'%(rtype,role)inself._related_cachedefrelated_cache(self,rtype,role,entities=True,limit=None):"""return values for the given relation if it's cached on the instance, else raise `KeyError` """res=self._related_cache['%s_%s'%(rtype,role)][entities]iflimit:ifentities:res=res[:limit]else:res=res.limit(limit)returnresdefset_related_cache(self,rtype,role,rset,col=0):"""set cached values for the given relation"""ifrset:related=list(rset.entities(col))rschema=self.schema.rschema(rtype)ifrole=='subject':rcard=rschema.rproperty(self.e_schema,related[0].e_schema,'cardinality')[1]target='object'else:rcard=rschema.rproperty(related[0].e_schema,self.e_schema,'cardinality')[0]target='subject'ifrcardin'?1':forrentityinrelated:rentity._related_cache['%s_%s'%(rtype,target)]=(self.as_rset(),[self])else:related=[]self._related_cache['%s_%s'%(rtype,role)]=(rset,related)defclear_related_cache(self,rtype=None,role=None):"""clear cached values for the given relation or the entire cache if no relation is given """ifrtypeisNone:self._related_cache={}else:assertroleself._related_cache.pop('%s_%s'%(rtype,role),None)# raw edition utilities ###################################################defset_attributes(self,**kwargs):assertkwargsrelations=[]forkeyinkwargs:relations.append('X %s%%(%s)s'%(key,key))kwargs['x']=self.eidself.req.execute('SET %s WHERE X eid %%(x)s'%','.join(relations),kwargs,'x')forkey,valinkwargs.iteritems():self[key]=valdefdelete(self):assertself.has_eid(),self.eidself.req.execute('DELETE %s X WHERE X eid %%(x)s'%self.e_schema,{'x':self.eid})# server side utilities ###################################################defset_defaults(self):"""set default values according to the schema"""self._default_set=set()forattr,valueinself.e_schema.defaults():ifnotself.has_key(attr):self[str(attr)]=valueself._default_set.add(attr)defcheck(self,creation=False):"""check this entity against its schema. Only final relation are checked here, constraint on actual relations are checked in hooks """# necessary since eid is handled specifically and yams require it to be# in the dictionaryifself.reqisNone:_=unicodeelse:_=self.req._self.e_schema.check(self,creation=creation,_=_)deffti_containers(self,_done=None):if_doneisNone:_done=set()_done.add(self.eid)containers=tuple(self.e_schema.fulltext_containers())ifcontainers:forrschema,targetincontainers:iftarget=='object':targets=getattr(self,rschema.type)else:targets=getattr(self,'reverse_%s'%rschema)forentityintargets:ifentity.eidin_done:continueforcontainerinentity.fti_containers(_done):yieldcontainerelse:yieldselfdefget_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 indexer package :rtype: list :return: the list of indexable word of this entity """fromindexer.query_objectsimporttokenizewords=[]forrschemainself.e_schema.indexable_attributes():try:value=self.printable_value(rschema,format='text/plain')exceptTransformError,ex:continueexcept:self.exception("can't add value of %s to text index for entity %s",rschema,self.eid)continueifvalue:words+=tokenize(value)forrschema,roleinself.e_schema.fulltext_relations():ifrole=='subject':forentityingetattr(self,rschema.type):words+=entity.get_words()else:# if role == 'object':forentityingetattr(self,'reverse_%s'%rschema.type):words+=entity.get_words()returnwords# attribute and relation descriptors ##########################################classAttribute(object):"""descriptor that controls schema attribute access"""def__init__(self,attrname):assertattrname!='eid'self._attrname=attrnamedef__get__(self,eobj,eclass):ifeobjisNone:returnselfreturneobj.get_value(self._attrname)def__set__(self,eobj,value):# XXX bw compat# would be better to generate UPDATE queries than the current behavioureobj.warning("deprecated usage, don't use 'entity.attr = val' notation)")eobj[self._attrname]=valueclassRelation(object):"""descriptor that controls schema relation access"""_role=None# for pylintdef__init__(self,rschema):self._rschema=rschemaself._rtype=rschema.typedef__get__(self,eobj,eclass):ifeobjisNone:raiseAttributeError('%s cannot be only be accessed from instances'%self._rtype)returneobj.related(self._rtype,self._role,entities=True)def__set__(self,eobj,value):raiseNotImplementedErrorclassSubjectRelation(Relation):"""descriptor that controls schema relation access"""_role='subject'classObjectRelation(Relation):"""descriptor that controls schema relation access"""_role='object'fromloggingimportgetLoggerfromcubicwebimportset_log_methodsset_log_methods(Entity,getLogger('cubicweb.entity'))