"""Base class for dynamically loaded objects manipulated in the web interface: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"fromwarningsimportwarnfrommx.DateTimeimportnow,oneSecondfromsimplejsonimportdumpsfromlogilab.common.deprecationimportobsoletefromrql.stmtsimportUnion,SelectfromcubicwebimportUnauthorizedfromcubicweb.vregistryimportVObjectfromcubicweb.common.utilsimportUStringIOfromcubicweb.common.uilibimporthtml_escape,ustrftimefromcubicweb.common.registerersimportyes_registerer,priority_registererfromcubicweb.common.selectorsimportyes_MARKER=object()classCache(dict):def__init__(self):super(Cache,self).__init__()self.cache_creation_date=Noneself.latest_cache_lookup=now()CACHE_REGISTRY={}classAppRsetObject(VObject):"""This is the base class for CubicWeb application objects which are selected according to a request and result set. Classes are kept in the vregistry and instantiation is done at selection time. At registration time, the following attributes are set on the class: :vreg: the application's registry :schema: the application's schema :config: the application's configuration At instantiation time, the following attributes are set on the instance: :req: current request :rset: result set on which the object is applied """@classmethoddefregistered(cls,vreg):cls.vreg=vregcls.schema=vreg.schemacls.config=vreg.configcls.register_properties()returncls@classmethoddefselected(cls,req,rset,row=None,col=None,**kwargs):"""by default web app objects are usually instantiated on selection according to a request, a result set, and optional row and col """instance=cls(req,rset)instance.row=rowinstance.col=colreturninstance# Eproperties definition:# key: id of the property (the actual EProperty key is build using# <registry name>.<obj id>.<property id># value: tuple (property type, vocabfunc, default value, property description)# possible types are those used by `logilab.common.configuration`## notice that when it exists multiple objects with the same id (adaptation,# overriding) only the first encountered definition is considered, so those# objects can't try to have different default values for instance.property_defs={}@classmethoddefregister_properties(cls):forpropid,pdefincls.property_defs.items():pdef=pdef.copy()# may be sharedpdef['default']=getattr(cls,propid,pdef['default'])pdef['sitewide']=getattr(cls,'site_wide',pdef.get('sitewide'))cls.vreg.register_property(cls.propkey(propid),**pdef)@classmethoddefpropkey(cls,propid):return'%s.%s.%s'%(cls.__registry__,cls.id,propid)def__init__(self,req,rset):super(AppRsetObject,self).__init__()self.req=reqself.rset=rset@propertydefcursor(self):# XXX deprecate in favor of req.cursor?msg='.cursor is deprecated, use req.execute (or req.cursor if necessary)'warn(msg,DeprecationWarning,stacklevel=2)returnself.req.cursordefget_cache(self,cachename):""" NOTE: cachename should be dotted names as in : - cubicweb.mycache - cubes.blog.mycache - etc. """ifcachenameinCACHE_REGISTRY:cache=CACHE_REGISTRY[cachename]else:cache=Cache()CACHE_REGISTRY[cachename]=cache_now=now()if_now>cache.latest_cache_lookup+oneSecond:ecache=self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T',{'name':cachename}).get_entity(0,0)cache.latest_cache_lookup=_nowifnotecache.valid(cache.cache_creation_date):cache.empty()cache.cache_creation_date=_nowreturncachedefpropval(self,propid):assertself.reqreturnself.req.property_value(self.propkey(propid))deflimited_rql(self):"""return a printable rql for the result set associated to the object, with limit/offset correctly set according to maximum page size and currently displayed page when necessary """# try to get page boundaries from the navigation component# XXX we should probably not have a ref to this component here (eg in# cubicweb.common)nav=self.vreg.select_component('navigation',self.req,self.rset)ifnav:start,stop=nav.page_boundaries()rql=self._limit_offset_rql(stop-start,start)# result set may have be limited manually in which case navigation won't# applyelifself.rset.limited:rql=self._limit_offset_rql(*self.rset.limited)# navigation component doesn't apply and rset has not been limited, no# need to limit queryelse:rql=self.rset.printable_rql()returnrqldef_limit_offset_rql(self,limit,offset):rqlst=self.rset.syntax_tree()iflen(rqlst.children)==1:select=rqlst.children[0]olimit,ooffset=select.limit,select.offsetselect.limit,select.offset=limit,offsetrql=rqlst.as_string(kwargs=self.rset.args)# restore original limit/offsetselect.limit,select.offset=olimit,ooffsetelse:newselect=Select()newselect.limit=limitnewselect.offset=offsetaliases=[VariableRef(newselect.get_variable(vref.name,i))fori,vrefinenumerate(rqlst.selection)]newselect.set_with([SubQuery(aliases,rqlst)],check=False)newunion=Union()newunion.append(newselect)rql=rqlst.as_string(kwargs=self.rset.args)rqlst.parent=Nonereturnrql# url generation methods ##################################################controller='view'defbuild_url(self,method=None,**kwargs):"""return an absolute URL using params dictionary key/values as URL parameters. Values are automatically URL quoted, and the publishing method to use may be specified or will be guessed. """# XXX I (adim) think that if method is passed explicitly, we should# not try to process it and directly call req.build_url()ifmethodisNone:method=self.controllerifmethod=='view'andself.req.from_controller()=='view'and \not'_restpath'inkwargs:method=self.req.relative_path(includeparams=False)or'view'returnself.req.build_url(method,**kwargs)# various resources accessors #############################################defetype_rset(self,etype,size=1):"""return a fake result set for a particular entity type"""msg='.etype_rset is deprecated, use req.etype_rset'warn(msg,DeprecationWarning,stacklevel=2)returnself.req.etype_rset(etype,size=1)defeid_rset(self,eid,etype=None):"""return a result set for the given eid"""msg='.eid_rset is deprecated, use req.eid_rset'warn(msg,DeprecationWarning,stacklevel=2)returnself.req.eid_rset(eid,etype)defentity(self,row,col=0):"""short cut to get an entity instance for a particular row/column (col default to 0) """returnself.rset.get_entity(row,col)defcomplete_entity(self,row,col=0,skip_bytes=True):"""short cut to get an completed entity instance for a particular row (all instance's attributes have been fetched) """entity=self.entity(row,col)entity.complete(skip_bytes=skip_bytes)returnentitydefuser_rql_callback(self,args,msg=None):"""register a user callback to execute some rql query and return an url to call it ready to be inserted in html """defrqlexec(req,rql,args=None,key=None):req.execute(rql,args,key)returnself.user_callback(rqlexec,args,msg)defuser_callback(self,cb,args,msg=None,nonify=False):"""register the given user callback and return an url to call it ready to be inserted in html """self.req.add_js('cubicweb.ajax.js')ifnonify:# XXX < 2.48.3 bw compatwarn('nonify argument is deprecated',DeprecationWarning,stacklevel=2)_cb=cbdefcb(*args):_cb(*args)cbname=self.req.register_onetime_callback(cb,*args)msg=dumps(msgor'')return"javascript:userCallbackThenReloadPage('%s', %s)"%(cbname,msg)# formating methods #######################################################deftal_render(self,template,variables):"""render a precompiled page template with variables in the given dictionary as context """fromcubicweb.common.talimportCubicWebContextcontext=CubicWebContext()context.update({'self':self,'rset':self.rset,'_':self.req._,'req':self.req,'user':self.req.user})context.update(variables)output=UStringIO()template.expand(context,output)returnoutput.getvalue()defformat_date(self,date,date_format=None,time=False):"""return a string for a mx date time according to application's configuration """ifdate:ifdate_formatisNone:iftime:date_format=self.req.property_value('ui.datetime-format')else:date_format=self.req.property_value('ui.date-format')returnustrftime(date,date_format)returnu''defformat_time(self,time):"""return a string for a mx date time according to application's configuration """iftime:returnustrftime(time,self.req.property_value('ui.time-format'))returnu''defformat_float(self,num):"""return a string for floating point number according to application's configuration """ifnum:returnself.req.property_value('ui.float-format')%numreturnu''# security related methods ################################################defensure_ro_rql(self,rql):"""raise an exception if the given rql is not a select query"""first=rql.split(' ',1)[0].lower()iffirstin('insert','set','delete'):raiseUnauthorized(self.req._('only select queries are authorized'))# .accepts handling utilities #############################################accepts=('Any',)@classmethoddefaccept_rset(cls,req,rset,row,col):"""apply the following rules: * if row is None, return the sum of values returned by the method for each entity's type in the result set. If any score is 0, return 0. * if row is specified, return the value returned by the method with the entity's type of this row """ifrowisNone:score=0foretypeinrset.column_types(0):accepted=cls.accept(req.user,etype)ifnotaccepted:return0score+=acceptedreturnscorereturncls.accept(req.user,rset.description[row][color0])@classmethoddefaccept(cls,user,etype):"""score etype, returning better score on exact match"""if'Any'incls.accepts:return1eschema=cls.schema.eschema(etype)matching_types=[e.typeforeineschema.ancestors()]matching_types.append(etype)forindex,basetypeinenumerate(matching_types):ifbasetypeincls.accepts:return2+indexreturn0# .rtype handling utilities ##############################################@classmethoddefrelation_possible(cls,etype):"""tell if a relation with etype entity is possible according to mixed class'.etype, .rtype and .target attributes XXX should probably be moved out to a function """schema=cls.schemartype=cls.rtypeeschema=schema.eschema(etype)ifhasattr(cls,'role'):role=cls.roleelifcls.target=='subject':role='object'else:role='subject'# check if this relation is possible according to the schematry:ifrole=='object':rschema=eschema.object_relation(rtype)else:rschema=eschema.subject_relation(rtype)exceptKeyError:returnFalseifhasattr(cls,'etype'):letype=cls.etypetry:ifrole=='object':returnetypeinrschema.objects(letype)else:returnetypeinrschema.subjects(letype)exceptKeyError,ex:returnFalsereturnTrue# XXX deprecated (since 2.43) ##########################@obsolete('use req.datadir_url')defdatadir_url(self):"""return url of the application's data directory"""returnself.req.datadir_url@obsolete('use req.external_resource()')defexternal_resource(self,rid,default=_MARKER):returnself.req.external_resource(rid,default)classAppObject(AppRsetObject):"""base class for application objects which are not selected according to a result set, only by their identifier. Those objects may not have req, rset and cursor set. """@classmethoddefselected(cls,*args,**kwargs):"""by default web app objects are usually instantiated on selection """returncls(*args,**kwargs)def__init__(self,req=None,rset=None,**kwargs):self.req=reqself.rset=rsetself.__dict__.update(kwargs)classReloadableMixIn(object):"""simple mixin for reloadable parts of UI"""defuser_callback(self,cb,args,msg=None,nonify=False):"""register the given user callback and return an url to call it ready to be inserted in html """self.req.add_js('cubicweb.ajax.js')ifnonify:_cb=cbdefcb(*args):_cb(*args)cbname=self.req.register_onetime_callback(cb,*args)returnself.build_js(cbname,html_escape(msgor''))defbuild_update_js_call(self,cbname,msg):rql=html_escape(self.rset.printable_rql())return"javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')"%(cbname,self.id,rql,msg,self.__registry__,self.div_id())defbuild_reload_js_call(self,cbname,msg):return"javascript:userCallbackThenReloadPage('%s', '%s')"%(cbname,msg)build_js=build_update_js_call# expect updatable component by defaultdefdiv_id(self):return''classComponentMixIn(ReloadableMixIn):"""simple mixin for component object"""__registry__='components'__registerer__=yes_registerer__selectors__=(yes,)__select__=classmethod(*__selectors__)defdiv_class(self):return'%s%s'%(self.propval('htmlclass'),self.id)defdiv_id(self):return'%sComponent'%self.idclassComponent(ComponentMixIn,AppObject):"""base class for non displayable components """classSingletonComponent(Component):"""base class for non displayable unique components """__registerer__=priority_registerer