# copyright 2003-2010 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/>."""abstract component class and base components definition for CubicWeb webclient"""__docformat__="restructuredtext en"_=unicodefromlogilab.common.deprecationimportclass_deprecated,class_renamedfromlogilab.mtconverterimportxml_escapefromcubicwebimportUnauthorized,role,tagsfromcubicweb.uilibimportjs,domidfromcubicweb.utilsimportjson_dumpsfromcubicweb.viewimportReloadableMixIn,Componentfromcubicweb.selectorsimport(no_cnx,paginated_rset,one_line_rset,non_final_entity,partial_relation_possible,partial_has_related_entities)fromcubicweb.appobjectimportAppObjectfromcubicweb.webimporthtmlwidgets,stdmsgs# abstract base class for navigation components ################################classNavigationComponent(Component):"""abstract base class for navigation components"""__regid__='navigation'__select__=paginated_rset()cw_property_defs={_('visible'):dict(type='Boolean',default=True,help=_('display the component or not')),}page_size_property='navigation.page-size'start_param='__start'stop_param='__stop'page_link_templ=u'<span class="slice"><a href="%s" title="%s">%s</a></span>'selected_page_link_templ=u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'previous_page_link_templ=next_page_link_templ=page_link_templno_previous_page_link=u'<<'no_next_page_link=u'>>'def__init__(self,req,rset,**kwargs):super(NavigationComponent,self).__init__(req,rset=rset,**kwargs)self.starting_from=0self.total=rset.rowcountdefget_page_size(self):try:returnself._page_sizeexceptAttributeError:page_size=self.cw_extra_kwargs.get('page_size')ifpage_sizeisNone:if'page_size'inself._cw.form:page_size=int(self._cw.form['page_size'])else:page_size=self._cw.property_value(self.page_size_property)self._page_size=page_sizereturnpage_sizedefset_page_size(self,page_size):self._page_size=page_sizepage_size=property(get_page_size,set_page_size)defpage_boundaries(self):try:stop=int(self._cw.form[self.stop_param])+1start=int(self._cw.form[self.start_param])exceptKeyError:start,stop=0,self.page_sizeifstart>=len(self.cw_rset):start,stop=0,self.page_sizeself.starting_from=startreturnstart,stopdefclean_params(self,params):ifself.start_paraminparams:delparams[self.start_param]ifself.stop_paraminparams:delparams[self.stop_param]defpage_url(self,path,params,start=None,stop=None):params=dict(params)ifstartisnotNone:params[self.start_param]=startifstopisnotNone:params[self.stop_param]=stopview=self.cw_extra_kwargs.get('view')ifviewisnotNoneandhasattr(view,'page_navigation_url'):url=view.page_navigation_url(self,path,params)elifpath=='json':url=self.ajax_page_url(**params)else:url=self._cw.build_url(path,**params)# XXX hack to avoid opening a new page containing the evaluation of the# js expression on ajax callifurl.startswith('javascript:'):url+='; noop();'returnurldefajax_page_url(self,**params):divid=params.setdefault('divid','pageContent')params['rql']=self.cw_rset.printable_rql()return"javascript: $(%s).loadxhtml('json', %s, 'get', 'swap')"%(json_dumps('#'+divid),js.ajaxFuncArgs('view',params))defpage_link(self,path,params,start,stop,content):url=xml_escape(self.page_url(path,params,start,stop))ifstart==self.starting_from:returnself.selected_page_link_templ%(url,content,content)returnself.page_link_templ%(url,content,content)defprevious_link(self,path,params,content='<<',title=_('previous_results')):start=self.starting_fromifnotstart:returnself.no_previous_page_linkstart=max(0,start-self.page_size)stop=start+self.page_size-1url=xml_escape(self.page_url(path,params,start,stop))returnself.previous_page_link_templ%(url,title,content)defnext_link(self,path,params,content='>>',title=_('next_results')):start=self.starting_from+self.page_sizeifstart>=self.total:returnself.no_next_page_linkstop=start+self.page_size-1url=xml_escape(self.page_url(path,params,start,stop))returnself.next_page_link_templ%(url,title,content)# new contextual components system #############################################defoverride_ctx(cls,**kwargs):cwpdefs=cls.cw_property_defs.copy()cwpdefs['context']=cwpdefs['context'].copy()cwpdefs['context'].update(kwargs)returncwpdefsclassEmptyComponent(Exception):"""some selectable component has actually no content and should not be rendered """classLayout(Component):__regid__='layout'__abstract__=Truedefinit_rendering(self):"""init view for rendering. Return true if we should go on, false if we should stop now. """view=self.cw_extra_kwargs['view']try:view.init_rendering()exceptUnauthorized,ex:self.warning("can't render %s: %s",view,ex)returnFalseexceptEmptyComponent:returnFalsereturnTrueclassCtxComponent(AppObject):"""base class for contextual compontents. The following contexts are predefined: * boxes: 'left', 'incontext', 'right' * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' * other: 'ctxtoolbar' The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' context are handled by the default primary view, others by the default main template. All subclasses may not support all those contexts (for instance if it can't be displayed as box, or as a toolbar icon). You may restrict allowed context as followed: .. sourcecode:: python class MyComponent(CtxComponent): cw_property_defs = override_ctx(CtxComponent, vocabulary=[list of contexts]) context = 'my default context' You can configure default component's context by simply giving appropriate value to the `context` class attribute, as seen above. """__registry__='ctxcomponents'__select__=~no_cnx()categories_in_order=()cw_property_defs={_('visible'):dict(type='Boolean',default=True,help=_('display the box or not')),_('order'):dict(type='Int',default=99,help=_('display order of the box')),_('context'):dict(type='String',default='left',vocabulary=(_('left'),_('incontext'),_('right'),_('navtop'),_('navbottom'),_('navcontenttop'),_('navcontentbottom'),_('ctxtoolbar')),help=_('context where this component should be displayed')),}context='left'contextual=Falsetitle=None# XXX support kwargs for compat with old boxes which gets the view as# argumentdefrender(self,w,**kwargs):getlayout=self._cw.vreg['components'].selecttry:# XXX ensure context is given when the component is reloaded through# ajaxcontext=self.cw_extra_kwargs['context']exceptKeyError:context=self.cw_propval('context')layout=getlayout('layout',self._cw,rset=self.cw_rset,row=self.cw_row,col=self.cw_col,view=self,context=context)layout.render(w)definit_rendering(self):"""init rendering callback: that's the good time to check your component has some content to display. If not, you can still raise :exc:`EmptyComponent` to inform it should be skipped. Also, :exc:`Unauthorized` will be catched, logged, then the component will be skipped. """self.items=[]@propertydefdomid(self):"""return the HTML DOM identifier for this component"""returndomid(self.__regid__)@propertydefcssclass(self):"""return the CSS class name for this component"""returndomid(self.__regid__)defrender_title(self,w):"""return the title for this component"""ifself.title:w(self._cw._(self.title))defrender_body(self,w):"""return the body (content) for this component"""raiseNotImplementedError()defrender_items(self,w,items=None,klass=u'boxListing'):ifitemsisNone:items=self.itemsassertitemsw(u'<ul class="%s">'%klass)foriteminitems:ifhasattr(item,'render'):item.render(w)# XXX display <li> by itselfelse:w(u'<li>')w(item)w(u'</li>')w(u'</ul>')defappend(self,item):self.items.append(item)defbox_action(self,action):# XXX action_linkreturnself.build_link(self._cw._(action.title),action.url())defbuild_link(self,title,url,**kwargs):ifself._cw.selected(url):try:kwargs['klass']+=' selected'exceptKeyError:kwargs['klass']='selected'returntags.a(title,href=url,**kwargs)classEntityCtxComponent(CtxComponent):"""base class for boxes related to a single entity"""__select__=CtxComponent.__select__&non_final_entity()&one_line_rset()context='incontext'contextual=Truedef__init__(self,*args,**kwargs):super(EntityCtxComponent,self).__init__(*args,**kwargs)try:entity=kwargs['entity']exceptKeyError:entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)self.entity=entity@propertydefdomid(self):returndomid(self.__regid__)+unicode(self.entity.eid)# high level abstract classes ##################################################classRQLCtxComponent(CtxComponent):"""abstract box for boxes displaying the content of a rql query not related to the current result set. """rql=Nonedefto_display_rql(self):assertself.rqlisnotNone,self.__regid__return(self.rql,)definit_rendering(self):rset=self._cw.execute(*self.to_display_rql())ifnotrset:raiseEmptyComponent()iflen(rset[0])==2:self.items=[]fori,(eid,label)inenumerate(rset):entity=rset.get_entity(i,0)self.items.append(self.build_link(label,entity.absolute_url()))else:self.items=[self.build_link(e.dc_title(),e.absolute_url())foreinrset.entities()]defrender_body(self,w):self.render_items(w)classEditRelationMixIn(ReloadableMixIn):defbox_item(self,entity,etarget,rql,label):"""builds HTML link to edit relation between `entity` and `etarget`"""role,target=role(self),get_target(self)args={role[0]:entity.eid,target[0]:etarget.eid}url=self._cw.user_rql_callback((rql,args))# for each target, provide a link to edit the relationreturnu'[<a href="%s">%s</a>] %s'%(xml_escape(url),label,etarget.view('incontext'))defrelated_boxitems(self,entity):rql='DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s'%self.rtypereturn[self.box_item(entity,etarget,rql,u'-')foretargetinself.related_entities(entity)]defrelated_entities(self,entity):returnentity.related(self.rtype,role(self),entities=True)defunrelated_boxitems(self,entity):rql='SET S %s O WHERE S eid %%(s)s, O eid %%(o)s'%self.rtypereturn[self.box_item(entity,etarget,rql,u'+')foretargetinself.unrelated_entities(entity)]defunrelated_entities(self,entity):"""returns the list of unrelated entities, using the entity's appropriate vocabulary function """skip=set(unicode(e.eid)foreinentity.related(self.rtype,role(self),entities=True))skip.add(None)skip.add(INTERNAL_FIELD_VALUE)filteretype=getattr(self,'etype',None)entities=[]form=self._cw.vreg['forms'].select('edition',self._cw,rset=self.cw_rset,row=self.cw_rowor0)field=form.field_by_name(self.rtype,role(self),entity.e_schema)for_,eidinfield.vocabulary(form):ifeidnotinskip:entity=self._cw.entity_from_eid(eid)iffilteretypeisNoneorentity.__regid__==filteretype:entities.append(entity)returnentitiesclassEditRelationCtxComponent(EditRelationMixIn,EntityCtxComponent):"""base class for boxes which let add or remove entities linked by a given relation subclasses should define at least id, rtype and target class attributes. """defrender_title(self,w):returndisplay_name(self._cw,self.rtype,role(self),context=self.entity.__regid__)defrender_body(self,w):self._cw.add_js('cubicweb.ajax.js')related=self.related_boxitems(self.entity)unrelated=self.unrelated_boxitems(self.entity)self.items.extend(related)ifrelatedandunrelated:self.items.append(htmlwidgets.BoxSeparator())self.items.extend(unrelated)self.render_items(w)classAjaxEditRelationCtxComponent(EntityCtxComponent):__select__=EntityCtxComponent.__select__&(partial_relation_possible(action='add')|partial_has_related_entities())# view used to display related enttiesitem_vid='incontext'# values separator when multiple values are allowedseparator=','# msgid of the message to display when some new relation has been added/removedadded_msg=Noneremoved_msg=None# class attributes below *must* be set in concret classes (additionaly to# rtype / role [/ target_etype]. They should correspond to js_* methods on# the json controller# function(eid)# -> expected to return a list of values to display as input selector# vocabularyfname_vocabulary=None# function(eid, value)# -> handle the selector's input (eg create necessary entities and/or# relations). If the relation is multiple, you'll get a list of value, else# a single string value.fname_validate=None# function(eid, linked entity eid)# -> remove the relationfname_remove=Nonedef__init__(self,*args,**kwargs):super(AjaxEditRelationCtxComponent,self).__init__(*args,**kwargs)self.rdef=self.entity.e_schema.rdef(self.rtype,self.role,self.target_etype)defrender_title(self,w):w(self.rdef.rtype.display_name(self._cw,self.role,context=self.entity.__regid__))defrender_body(self,w):req=self._cwentity=self.entityrelated=entity.related(self.rtype,self.role)ifself.role=='subject':mayadd=self.rdef.has_perm(req,'add',fromeid=entity.eid)maydel=self.rdef.has_perm(req,'delete',fromeid=entity.eid)else:mayadd=self.rdef.has_perm(req,'add',toeid=entity.eid)maydel=self.rdef.has_perm(req,'delete',toeid=entity.eid)ifmayaddormaydel:req.add_js(('cubicweb.ajax.js','cubicweb.ajax.box.js'))_=req._ifrelated:w(u'<table>')forrentityinrelated.entities():# for each related entity, provide a link to remove the relationsubview=rentity.view(self.item_vid)ifmaydel:jscall=unicode(js.ajaxBoxRemoveLinkedEntity(self.__regid__,entity.eid,rentity.eid,self.fname_remove,self.removed_msgand_(self.removed_msg)))w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>''<td class="tagged"> %s</td></tr>'%(xml_escape(jscall),subview))else:w(u'<tr><td class="tagged">%s</td></tr>'%(subview))w(u'</table>')else:w(_('no related entity'))ifmayadd:req.add_js('jquery.autocomplete.js')req.add_css('jquery.autocomplete.css')multiple=self.rdef.role_cardinality(self.role)in'*+'w(u'<table><tr><td>')jscall=unicode(js.ajaxBoxShowSelector(self.__regid__,entity.eid,self.fname_vocabulary,self.fname_validate,self.added_msgand_(self.added_msg),_(stdmsgs.BUTTON_OK[0]),_(stdmsgs.BUTTON_CANCEL[0]),multipleandself.separator))w('<a class="button sglink" href="javascript: %s">%s</a>'%(xml_escape(jscall),multipleand_('add_relation')or_('update_relation')))w(u'</td><td>')w(u'<div id="%sHolder"></div>'%self.domid)w(u'</td></tr></table>')# old contextual components, deprecated ########################################classEntityVComponent(Component):"""abstract base class for additinal components displayed in content headers and footer according to: * the displayed entity's type * a context (currently 'header' or 'footer') it should be configured using .accepts, .etype, .rtype, .target and .context class attributes """__metaclass__=class_deprecated__deprecation_warning__='[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'__registry__='ctxcomponents'__select__=one_line_rset()cw_property_defs={_('visible'):dict(type='Boolean',default=True,help=_('display the component or not')),_('order'):dict(type='Int',default=99,help=_('display order of the component')),_('context'):dict(type='String',default='navtop',vocabulary=(_('navtop'),_('navbottom'),_('navcontenttop'),_('navcontentbottom'),_('ctxtoolbar')),help=_('context where this component should be displayed')),}context='navcontentbottom'defcall(self,view=None):ifself.cw_rsetisNone:self.entity_call(self.cw_extra_kwargs.pop('entity'))else:self.cell_call(0,0,view=view)defcell_call(self,row,col,view=None):self.entity_call(self.cw_rset.get_entity(row,col),view=view)defentity_call(self,entity,view=None):raiseNotImplementedError()classRelatedObjectsVComponent(EntityVComponent):"""a section to display some related entities"""__select__=EntityVComponent.__select__&partial_has_related_entities()vid='list'defrql(self):"""override this method if you want to use a custom rql query"""returnNonedefcell_call(self,row,col,view=None):rql=self.rql()ifrqlisNone:entity=self.cw_rset.get_entity(row,col)rset=entity.related(self.rtype,role(self))else:eid=self.cw_rset[row][col]rset=self._cw.execute(self.rql(),{'x':eid})ifnotrset.rowcount:returnself.w(u'<div class="%s">'%self.cssclass)self.w(u'<h4>%s</h4>\n'%self._cw._(self.title).capitalize())self.wview(self.vid,rset)self.w(u'</div>')VComponent=class_renamed('VComponent',Component,'[3.2] VComponent is deprecated, use Component')SingletonVComponent=class_renamed('SingletonVComponent',Component,'[3.2] SingletonVComponent is deprecated, use ''Component and explicit registration control')