[book] typos / clarifications in create-instance section
Clarify that cubicweb-ctl upgrade needs to be run whenever cubicweb or a
cube is upgraded. Related to #2632425.
# copyright 2003-2014 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"_=unicodefromwarningsimportwarnfromlogilab.common.deprecationimportclass_deprecated,class_renamed,deprecatedfromlogilab.mtconverterimportxml_escapefromcubicwebimportUnauthorized,role,target,tagsfromcubicweb.schemaimportdisplay_namefromcubicweb.uilibimportjs,domidfromcubicweb.utilsimportjson_dumps,js_hreffromcubicweb.viewimportReloadableMixIn,Componentfromcubicweb.predicatesimport(no_cnx,paginated_rset,one_line_rset,non_final_entity,partial_relation_possible,partial_has_related_entities)fromcubicweb.appobjectimportAppObjectfromcubicweb.webimportINTERNAL_FIELD_VALUE,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_templdef__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)params['__fromnavigation']=1ifstartisnotNone: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)elifpathin('json','ajax'):# 'ajax' is the new correct controller, but the old 'json'# controller should still be supportedurl=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()returnjs_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %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)@propertydefprev_icon_url(self):returnxml_escape(self._cw.data_url('go_prev.png'))@propertydefnext_icon_url(self):returnxml_escape(self._cw.data_url('go_next.png'))@propertydefno_previous_page_link(self):return(u'<img src="%s" alt="%s" class="prevnext_nogo"/>'%(self.prev_icon_url,self._cw._('there is no previous page')))@propertydefno_next_page_link(self):return(u'<img src="%s" alt="%s" class="prevnext_nogo"/>'%(self.next_icon_url,self._cw._('there is no next page')))@propertydefno_content_prev_link(self):return(u'<img src="%s" alt="%s" class="prevnext"/>'%((self.prev_icon_url,self._cw._('no content prev link'))))@propertydefno_content_next_link(self):return(u'<img src="%s" alt="%s" class="prevnext"/>'%(self.next_icon_url,self._cw._('no content next link')))defprevious_link(self,path,params,content=None,title=_('previous_results')):ifnotcontent:content=self.no_content_prev_linkstart=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,self._cw._(title),content)defnext_link(self,path,params,content=None,title=_('next_results')):ifnotcontent:content=self.no_content_next_linkstart=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,self._cw._(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 """classLink(object):"""a link to a view or action in the ui. Use this rather than `cw.web.htmlwidgets.BoxLink`. Note this class could probably be avoided with a proper DOM on the server side. """newstyle=Truedef__init__(self,href,label,**attrs):self.href=hrefself.label=labelself.attrs=attrsdef__unicode__(self):returntags.a(self.label,href=self.href,**self.attrs)defrender(self,w):w(tags.a(self.label,href=self.href,**self.attrs))def__repr__(self):return'<%s: href=%r label=%r%r>'%(self.__class__.__name__,self.href,self.label,self.attrs)classSeparator(object):"""a menu separator. Use this rather than `cw.web.htmlwidgets.BoxSeparator`. """newstyle=Truedefrender(self,w):w(u'<hr class="boxSeparator"/>')def_bwcompatible_render_item(w,item):ifhasattr(item,'render'):ifgetattr(item,'newstyle',False):ifisinstance(item,Separator):w(u'</ul>')item.render(w)w(u'<ul>')else:w(u'<li>')item.render(w)w(u'</li>')else:item.render(w)# XXX displays <li> by itselfelse:w(u'<li>%s</li>'%item)classLayout(Component):__regid__='component_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()exceptUnauthorizedasex:self.warning("can't render %s: %s",view,ex)returnFalseexceptEmptyComponent:returnFalsereturnTrueclassLayoutableMixIn(object):layout_id=None# to be defined in concret classlayout_args={}deflayout_render(self,w,**kwargs):getlayout=self._cw.vreg['components'].selectlayout=getlayout(self.layout_id,self._cw,**self.layout_select_args())layout.render(w)deflayout_select_args(self):args=dict(rset=self.cw_rset,row=self.cw_row,col=self.cw_col,view=self)args.update(self.layout_args)returnargsclassCtxComponent(LayoutableMixIn,AppObject):"""base class for contextual components. The following contexts are predefined: * boxes: 'left', 'incontext', 'right' * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' * other: 'ctxtoolbar' The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' contexts 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 follows: .. sourcecode:: python class MyComponent(CtxComponent): cw_property_defs = override_ctx(CtxComponent, vocabulary=[list of contexts]) context = 'my default context' You can configure a component's default context by simply giving an 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')),}visible=Trueorder=0context='left'contextual=Falsetitle=Nonelayout_id='component_layout'defrender(self,w,**kwargs):self.layout_render(w,**kwargs)deflayout_select_args(self):args=super(CtxComponent,self).layout_select_args()try:# XXX ensure context is given when the component is reloaded through# ajaxargs['context']=self.cw_extra_kwargs['context']exceptKeyError:args['context']=self.cw_propval('context')returnargsdefinit_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:_bwcompatible_render_item(w,item)w(u'</ul>')defappend(self,item):self.items.append(item)defaction_link(self,action):returnself.link(self._cw._(action.title),action.url())deflink(self,title,url,**kwargs):ifself._cw.selected(url):try:kwargs['klass']+=' selected'exceptKeyError:kwargs['klass']='selected'returnLink(url,title,**kwargs)defseparator(self):returnSeparator()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=entitydeflayout_select_args(self):args=super(EntityCtxComponent,self).layout_select_args()args['entity']=self.entityreturnargs@propertydefdomid(self):returndomid(self.__regid__)+unicode(self.entity.eid)deflazy_view_holder(self,w,entity,oid,registry='views'):"""add a holder and return a URL that may be used to replace this holder by the html generate by the view specified by registry and identifier. Registry defaults to 'views'. """holderid='%sHolder'%self.domidw(u'<div id="%s"></div>'%holderid)params=self.cw_extra_kwargs.copy()params.pop('view',None)params.pop('entity',None)form=params.pop('formparams',{})ifentity.has_eid():eid=entity.eidelse:eid=Noneform['etype']=entity.cw_etypeform['tempEid']=entity.eidargs=[json_dumps(x)forxin(registry,oid,eid,params)]returnself._cw.ajax_replace_url(holderid,fname='render',arg=args,**form)# high level abstract classes ##################################################classRQLCtxComponent(CtxComponent):"""abstract box for boxes displaying the content of a rql query not related to the current result set. Notice that this class's init_rendering implemention is overwriting context result set (eg `cw_rset`) with the result set returned by execution of `to_display_rql()`. """rql=Nonedefto_display_rql(self):"""return arguments to give to self._cw.execute, as a tuple, to build the result set to be displayed by this box. """assertself.rqlisnotNone,self.__regid__return(self.rql,)definit_rendering(self):super(RQLCtxComponent,self).init_rendering()self.cw_rset=self._cw.execute(*self.to_display_rql())ifnotself.cw_rset:raiseEmptyComponent()defrender_body(self,w):rset=self.cw_rsetiflen(rset[0])==2:items=[]fori,(eid,label)inenumerate(rset):entity=rset.get_entity(i,0)items.append(self.link(label,entity.absolute_url()))else:items=[self.link(e.dc_title(),e.absolute_url())foreinrset.entities()]self.render_items(w,items)classEditRelationMixIn(ReloadableMixIn):defbox_item(self,entity,etarget,fname,label):"""builds HTML link to edit relation between `entity` and `etarget`"""args={role(self):entity.eid,target(self):etarget.eid}# for each target, provide a link to edit the relationjscall=js.cw.utils.callAjaxFuncThenReload(fname,self.rtype,args['subject'],args['object'])returnu'[<a href="javascript: %s" class="action">%s</a>] %s'%(xml_escape(unicode(jscall)),label,etarget.view('incontext'))defrelated_boxitems(self,entity):return[self.box_item(entity,etarget,'delete_relation',u'-')foretargetinself.related_entities(entity)]defrelated_entities(self,entity):returnentity.related(self.rtype,role(self),entities=True)defunrelated_boxitems(self,entity):return[self.box_item(entity,etarget,'add_relation',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.cw_etype==filteretype:entities.append(entity)returnentities# XXX should be a view usable using uicfgclassEditRelationCtxComponent(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. """# to be defined in concrete classesrtype=Nonedefrender_title(self,w):w(display_name(self._cw,self.rtype,role(self),context=self.entity.cw_etype))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(u'<hr class="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# to be defined in concrete classesrtype=role=target_etype=None# class attributes below *must* be set in concrete 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.cw_etype))defadd_js_css(self):self._cw.add_js(('jquery.ui.js','cubicweb.widgets.js'))self._cw.add_js(('cubicweb.ajax.js','cubicweb.ajax.box.js'))self._cw.add_css('jquery.ui.css')returnTruedefrender_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)else:mayadd=self.rdef.has_perm(req,'add',toeid=entity.eid)js_css_added=Falseifmayadd:js_css_added=self.add_js_css()_=req._ifrelated:maydel=Nonew(u'<table class="ajaxEditRelationTable">')forrentityinrelated.entities():ifmaydelisNone:# Only check permission for the first related.ifself.role=='subject':fromeid,toeid=entity.eid,rentity.eidelse:fromeid,toeid=rentity.eid,entity.eidmaydel=self.rdef.has_perm(req,'delete',fromeid=fromeid,toeid=toeid)# for each related entity, provide a link to remove the relationsubview=rentity.view(self.item_vid)ifmaydel:ifnotjs_css_added:js_css_added=self.add_js_css()jscall=unicode(js.ajaxBoxRemoveLinkedEntity(self.__regid__,entity.eid,rentity.eid,self.fname_remove,self.removed_msgand_(self.removed_msg)))w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>''<td class="entity"> %s</td></tr>'%(xml_escape(jscall),subview))else:w(u'<tr><td class="entity">%s</td></tr>'%(subview))w(u'</table>')else:w(_('no related entity'))ifmayadd: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>')classRelatedObjectsCtxComponent(EntityCtxComponent):"""a contextual component to display entities related to another"""__select__=EntityCtxComponent.__select__&partial_has_related_entities()context='navcontentbottom'rtype=Nonerole='subject'vid='list'defrender_body(self,w):rset=self.entity.related(self.rtype,role(self))self._cw.view(self.vid,rset,w=w)# 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'# to be defined in concrete classesrtype=title=Nonedefrql(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>')