web/component.py
brancholdstable
changeset 7074 e4580e5f0703
parent 6987 d62d4ba5ef3e
child 7323 53f38d39287f
child 7516 9989b69e2a19
equal deleted inserted replaced
6749:48f468f33704 7074:e4580e5f0703
    20 """
    20 """
    21 
    21 
    22 __docformat__ = "restructuredtext en"
    22 __docformat__ = "restructuredtext en"
    23 _ = unicode
    23 _ = unicode
    24 
    24 
    25 from logilab.common.deprecation import class_renamed
    25 from warnings import warn
       
    26 
       
    27 from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
    26 from logilab.mtconverter import xml_escape
    28 from logilab.mtconverter import xml_escape
    27 
    29 
    28 from cubicweb import role
    30 from cubicweb import Unauthorized, role, target, tags
       
    31 from cubicweb.schema import display_name
       
    32 from cubicweb.uilib import js, domid
    29 from cubicweb.utils import json_dumps
    33 from cubicweb.utils import json_dumps
    30 from cubicweb.uilib import js
    34 from cubicweb.view import ReloadableMixIn, Component
    31 from cubicweb.view import Component
    35 from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
    32 from cubicweb.selectors import (
    36                                 non_final_entity, partial_relation_possible,
    33     paginated_rset, one_line_rset, primary_view, match_context_prop,
    37                                 partial_has_related_entities)
    34     partial_has_related_entities)
    38 from cubicweb.appobject import AppObject
    35 
    39 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
    36 
    40 
    37 class EntityVComponent(Component):
    41 
    38     """abstract base class for additinal components displayed in content
    42 # abstract base class for navigation components ################################
    39     headers and footer according to:
       
    40 
       
    41     * the displayed entity's type
       
    42     * a context (currently 'header' or 'footer')
       
    43 
       
    44     it should be configured using .accepts, .etype, .rtype, .target and
       
    45     .context class attributes
       
    46     """
       
    47 
       
    48     __registry__ = 'contentnavigation'
       
    49     __select__ = one_line_rset() & primary_view() & match_context_prop()
       
    50 
       
    51     cw_property_defs = {
       
    52         _('visible'):  dict(type='Boolean', default=True,
       
    53                             help=_('display the component or not')),
       
    54         _('order'):    dict(type='Int', default=99,
       
    55                             help=_('display order of the component')),
       
    56         _('context'):  dict(type='String', default='navtop',
       
    57                             vocabulary=(_('navtop'), _('navbottom'),
       
    58                                         _('navcontenttop'), _('navcontentbottom'),
       
    59                                         _('ctxtoolbar')),
       
    60                             help=_('context where this component should be displayed')),
       
    61     }
       
    62 
       
    63     context = 'navcontentbottom'
       
    64 
       
    65     def call(self, view=None):
       
    66         if self.cw_rset is None:
       
    67             self.entity_call(self.cw_extra_kwargs.pop('entity'))
       
    68         else:
       
    69             self.cell_call(0, 0, view=view)
       
    70 
       
    71     def cell_call(self, row, col, view=None):
       
    72         self.entity_call(self.cw_rset.get_entity(row, col), view=view)
       
    73 
       
    74     def entity_call(self, entity, view=None):
       
    75         raise NotImplementedError()
       
    76 
       
    77 
    43 
    78 class NavigationComponent(Component):
    44 class NavigationComponent(Component):
    79     """abstract base class for navigation components"""
    45     """abstract base class for navigation components"""
    80     __regid__ = 'navigation'
    46     __regid__ = 'navigation'
    81     __select__ = paginated_rset()
    47     __select__ = paginated_rset()
   148         else:
   114         else:
   149             url = self._cw.build_url(path, **params)
   115             url = self._cw.build_url(path, **params)
   150         # XXX hack to avoid opening a new page containing the evaluation of the
   116         # XXX hack to avoid opening a new page containing the evaluation of the
   151         # js expression on ajax call
   117         # js expression on ajax call
   152         if url.startswith('javascript:'):
   118         if url.startswith('javascript:'):
   153             url += '; noop();'
   119             url += '; $.noop();'
   154         return url
   120         return url
   155 
   121 
   156     def ajax_page_url(self, **params):
   122     def ajax_page_url(self, **params):
   157         divid = params.setdefault('divid', 'pageContent')
   123         divid = params.setdefault('divid', 'pageContent')
   158         params['rql'] = self.cw_rset.printable_rql()
   124         params['rql'] = self.cw_rset.printable_rql()
   179         if start >= self.total:
   145         if start >= self.total:
   180             return self.no_next_page_link
   146             return self.no_next_page_link
   181         stop = start + self.page_size - 1
   147         stop = start + self.page_size - 1
   182         url = xml_escape(self.page_url(path, params, start, stop))
   148         url = xml_escape(self.page_url(path, params, start, stop))
   183         return self.next_page_link_templ % (url, title, content)
   149         return self.next_page_link_templ % (url, title, content)
       
   150 
       
   151 
       
   152 # new contextual components system #############################################
       
   153 
       
   154 def override_ctx(cls, **kwargs):
       
   155     cwpdefs = cls.cw_property_defs.copy()
       
   156     cwpdefs['context']  = cwpdefs['context'].copy()
       
   157     cwpdefs['context'].update(kwargs)
       
   158     return cwpdefs
       
   159 
       
   160 
       
   161 class EmptyComponent(Exception):
       
   162     """some selectable component has actually no content and should not be
       
   163     rendered
       
   164     """
       
   165 
       
   166 
       
   167 class Link(object):
       
   168     """a link to a view or action in the ui.
       
   169 
       
   170     Use this rather than `cw.web.htmlwidgets.BoxLink`.
       
   171 
       
   172     Note this class could probably be avoided with a proper DOM on the server
       
   173     side.
       
   174     """
       
   175     newstyle = True
       
   176 
       
   177     def __init__(self, href, label, **attrs):
       
   178         self.href = href
       
   179         self.label = label
       
   180         self.attrs = attrs
       
   181 
       
   182     def __unicode__(self):
       
   183         return tags.a(self.label, href=self.href, **self.attrs)
       
   184 
       
   185     def render(self, w):
       
   186         w(tags.a(self.label, href=self.href, **self.attrs))
       
   187 
       
   188 
       
   189 class Separator(object):
       
   190     """a menu separator.
       
   191 
       
   192     Use this rather than `cw.web.htmlwidgets.BoxSeparator`.
       
   193     """
       
   194     newstyle = True
       
   195 
       
   196     def render(self, w):
       
   197         w(u'<hr class="boxSeparator"/>')
       
   198 
       
   199 
       
   200 def _bwcompatible_render_item(w, item):
       
   201     if hasattr(item, 'render'):
       
   202         if getattr(item, 'newstyle', False):
       
   203             if isinstance(item, Separator):
       
   204                 w(u'</ul>')
       
   205                 item.render(w)
       
   206                 w(u'<ul>')
       
   207             else:
       
   208                 w(u'<li>')
       
   209                 item.render(w)
       
   210                 w(u'</li>')
       
   211         else:
       
   212             item.render(w) # XXX displays <li> by itself
       
   213     else:
       
   214         w(u'<li>%s</li>' % item)
       
   215 
       
   216 
       
   217 class Layout(Component):
       
   218     __regid__ = 'layout'
       
   219     __abstract__ = True
       
   220 
       
   221     def init_rendering(self):
       
   222         """init view for rendering. Return true if we should go on, false
       
   223         if we should stop now.
       
   224         """
       
   225         view = self.cw_extra_kwargs['view']
       
   226         try:
       
   227             view.init_rendering()
       
   228         except Unauthorized, ex:
       
   229             self.warning("can't render %s: %s", view, ex)
       
   230             return False
       
   231         except EmptyComponent:
       
   232             return False
       
   233         return True
       
   234 
       
   235 
       
   236 class CtxComponent(AppObject):
       
   237     """base class for contextual components. The following contexts are
       
   238     predefined:
       
   239 
       
   240     * boxes: 'left', 'incontext', 'right'
       
   241     * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom'
       
   242     * other: 'ctxtoolbar'
       
   243 
       
   244     The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar'
       
   245     contexts are handled by the default primary view, others by the default main
       
   246     template.
       
   247 
       
   248     All subclasses may not support all those contexts (for instance if it can't
       
   249     be displayed as box, or as a toolbar icon). You may restrict allowed context
       
   250     as follows:
       
   251 
       
   252     .. sourcecode:: python
       
   253 
       
   254       class MyComponent(CtxComponent):
       
   255           cw_property_defs = override_ctx(CtxComponent,
       
   256                                           vocabulary=[list of contexts])
       
   257           context = 'my default context'
       
   258 
       
   259     You can configure a component's default context by simply giving an
       
   260     appropriate value to the `context` class attribute, as seen above.
       
   261     """
       
   262     __registry__ = 'ctxcomponents'
       
   263     __select__ = ~no_cnx()
       
   264 
       
   265     categories_in_order = ()
       
   266     cw_property_defs = {
       
   267         _('visible'): dict(type='Boolean', default=True,
       
   268                            help=_('display the box or not')),
       
   269         _('order'):   dict(type='Int', default=99,
       
   270                            help=_('display order of the box')),
       
   271         _('context'): dict(type='String', default='left',
       
   272                            vocabulary=(_('left'), _('incontext'), _('right'),
       
   273                                        _('navtop'), _('navbottom'),
       
   274                                        _('navcontenttop'), _('navcontentbottom'),
       
   275                                        _('ctxtoolbar')),
       
   276                            help=_('context where this component should be displayed')),
       
   277         }
       
   278     visible = True
       
   279     order = 0
       
   280     context = 'left'
       
   281     contextual = False
       
   282     title = None
       
   283 
       
   284     # XXX support kwargs for compat with old boxes which gets the view as
       
   285     # argument
       
   286     def render(self, w, **kwargs):
       
   287         if hasattr(self, 'call'):
       
   288             warn('[3.10] should not anymore implement call on %s, see new CtxComponent api'
       
   289                  % self.__class__, DeprecationWarning)
       
   290             self.w = w
       
   291             def wview(__vid, rset=None, __fallback_vid=None, **kwargs):
       
   292                 self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
       
   293             self.wview = wview
       
   294             self.call(**kwargs)
       
   295             return
       
   296         getlayout = self._cw.vreg['components'].select
       
   297         layout = getlayout('layout', self._cw, **self.layout_select_args())
       
   298         layout.render(w)
       
   299 
       
   300     def layout_select_args(self):
       
   301         try:
       
   302             # XXX ensure context is given when the component is reloaded through
       
   303             # ajax
       
   304             context = self.cw_extra_kwargs['context']
       
   305         except KeyError:
       
   306             context = self.cw_propval('context')
       
   307         return dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col,
       
   308                     view=self, context=context)
       
   309 
       
   310     def init_rendering(self):
       
   311         """init rendering callback: that's the good time to check your component
       
   312         has some content to display. If not, you can still raise
       
   313         :exc:`EmptyComponent` to inform it should be skipped.
       
   314 
       
   315         Also, :exc:`Unauthorized` will be catched, logged, then the component
       
   316         will be skipped.
       
   317         """
       
   318         self.items = []
       
   319 
       
   320     @property
       
   321     def domid(self):
       
   322         """return the HTML DOM identifier for this component"""
       
   323         return domid(self.__regid__)
       
   324 
       
   325     @property
       
   326     def cssclass(self):
       
   327         """return the CSS class name for this component"""
       
   328         return domid(self.__regid__)
       
   329 
       
   330     def render_title(self, w):
       
   331         """return the title for this component"""
       
   332         if self.title:
       
   333             w(self._cw._(self.title))
       
   334 
       
   335     def render_body(self, w):
       
   336         """return the body (content) for this component"""
       
   337         raise NotImplementedError()
       
   338 
       
   339     def render_items(self, w, items=None, klass=u'boxListing'):
       
   340         if items is None:
       
   341             items = self.items
       
   342         assert items
       
   343         w(u'<ul class="%s">' % klass)
       
   344         for item in items:
       
   345             _bwcompatible_render_item(w, item)
       
   346         w(u'</ul>')
       
   347 
       
   348     def append(self, item):
       
   349         self.items.append(item)
       
   350 
       
   351     def action_link(self, action):
       
   352         return self.link(self._cw._(action.title), action.url())
       
   353 
       
   354     def link(self, title, url, **kwargs):
       
   355         if self._cw.selected(url):
       
   356             try:
       
   357                 kwargs['klass'] += ' selected'
       
   358             except KeyError:
       
   359                 kwargs['klass'] = 'selected'
       
   360         return Link(url, title, **kwargs)
       
   361 
       
   362     def separator(self):
       
   363         return Separator()
       
   364 
       
   365     @deprecated('[3.10] use action_link() / link()')
       
   366     def box_action(self, action): # XXX action_link
       
   367         return self.build_link(self._cw._(action.title), action.url())
       
   368 
       
   369     @deprecated('[3.10] use action_link() / link()')
       
   370     def build_link(self, title, url, **kwargs):
       
   371         if self._cw.selected(url):
       
   372             try:
       
   373                 kwargs['klass'] += ' selected'
       
   374             except KeyError:
       
   375                 kwargs['klass'] = 'selected'
       
   376         return tags.a(title, href=url, **kwargs)
       
   377 
       
   378 
       
   379 class EntityCtxComponent(CtxComponent):
       
   380     """base class for boxes related to a single entity"""
       
   381     __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset()
       
   382     context = 'incontext'
       
   383     contextual = True
       
   384 
       
   385     def __init__(self, *args, **kwargs):
       
   386         super(EntityCtxComponent, self).__init__(*args, **kwargs)
       
   387         try:
       
   388             entity = kwargs['entity']
       
   389         except KeyError:
       
   390             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   391         self.entity = entity
       
   392 
       
   393     def layout_select_args(self):
       
   394         args = super(EntityCtxComponent, self).layout_select_args()
       
   395         args['entity'] = self.entity
       
   396         return args
       
   397 
       
   398     @property
       
   399     def domid(self):
       
   400         return domid(self.__regid__) + unicode(self.entity.eid)
       
   401 
       
   402     def lazy_view_holder(self, w, entity, oid, registry='views'):
       
   403         """add a holder and return an url that may be used to replace this
       
   404         holder by the html generate by the view specified by registry and
       
   405         identifier. Registry defaults to 'views'.
       
   406         """
       
   407         holderid = '%sHolder' % self.domid
       
   408         w(u'<div id="%s"></div>' % holderid)
       
   409         params = self.cw_extra_kwargs.copy()
       
   410         params.pop('view', None)
       
   411         params.pop('entity', None)
       
   412         form = params.pop('formparams', {})
       
   413         form['pageid'] = self._cw.pageid
       
   414         if entity.has_eid():
       
   415             eid = entity.eid
       
   416         else:
       
   417             eid = None
       
   418             form['etype'] = entity.__regid__
       
   419             form['tempEid'] = entity.eid
       
   420         args = [json_dumps(x) for x in (registry, oid, eid, params)]
       
   421         return self._cw.ajax_replace_url(
       
   422             holderid, fname='render', arg=args, **form)
       
   423 
       
   424 
       
   425 # high level abstract classes ##################################################
       
   426 
       
   427 class RQLCtxComponent(CtxComponent):
       
   428     """abstract box for boxes displaying the content of a rql query not related
       
   429     to the current result set.
       
   430 
       
   431     Notice that this class's init_rendering implemention is overwriting context
       
   432     result set (eg `cw_rset`) with the result set returned by execution of
       
   433     `to_display_rql()`.
       
   434     """
       
   435     rql = None
       
   436 
       
   437     def to_display_rql(self):
       
   438         """return arguments to give to self._cw.execute, as a tuple, to build
       
   439         the result set to be displayed by this box.
       
   440         """
       
   441         assert self.rql is not None, self.__regid__
       
   442         return (self.rql,)
       
   443 
       
   444     def init_rendering(self):
       
   445         super(RQLCtxComponent, self).init_rendering()
       
   446         self.cw_rset = self._cw.execute(*self.to_display_rql())
       
   447         if not self.cw_rset:
       
   448             raise EmptyComponent()
       
   449 
       
   450     def render_body(self, w):
       
   451         rset = self.cw_rset
       
   452         if len(rset[0]) == 2:
       
   453             items = []
       
   454             for i, (eid, label) in enumerate(rset):
       
   455                 entity = rset.get_entity(i, 0)
       
   456                 items.append(self.link(label, entity.absolute_url()))
       
   457         else:
       
   458             items = [self.link(e.dc_title(), e.absolute_url())
       
   459                      for e in rset.entities()]
       
   460         self.render_items(w, items)
       
   461 
       
   462 
       
   463 class EditRelationMixIn(ReloadableMixIn):
       
   464     def box_item(self, entity, etarget, rql, label):
       
   465         """builds HTML link to edit relation between `entity` and `etarget`"""
       
   466         args = {role(self)[0] : entity.eid, target(self)[0] : etarget.eid}
       
   467         url = self._cw.user_rql_callback((rql, args))
       
   468         # for each target, provide a link to edit the relation
       
   469         return u'[<a href="%s" class="action">%s</a>] %s' % (
       
   470             xml_escape(url), label, etarget.view('incontext'))
       
   471 
       
   472     def related_boxitems(self, entity):
       
   473         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
   474         return [self.box_item(entity, etarget, rql, u'-')
       
   475                 for etarget in self.related_entities(entity)]
       
   476 
       
   477     def related_entities(self, entity):
       
   478         return entity.related(self.rtype, role(self), entities=True)
       
   479 
       
   480     def unrelated_boxitems(self, entity):
       
   481         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
   482         return [self.box_item(entity, etarget, rql, u'+')
       
   483                 for etarget in self.unrelated_entities(entity)]
       
   484 
       
   485     def unrelated_entities(self, entity):
       
   486         """returns the list of unrelated entities, using the entity's
       
   487         appropriate vocabulary function
       
   488         """
       
   489         skip = set(unicode(e.eid) for e in entity.related(self.rtype, role(self),
       
   490                                                           entities=True))
       
   491         skip.add(None)
       
   492         skip.add(INTERNAL_FIELD_VALUE)
       
   493         filteretype = getattr(self, 'etype', None)
       
   494         entities = []
       
   495         form = self._cw.vreg['forms'].select('edition', self._cw,
       
   496                                              rset=self.cw_rset,
       
   497                                              row=self.cw_row or 0)
       
   498         field = form.field_by_name(self.rtype, role(self), entity.e_schema)
       
   499         for _, eid in field.vocabulary(form):
       
   500             if eid not in skip:
       
   501                 entity = self._cw.entity_from_eid(eid)
       
   502                 if filteretype is None or entity.__regid__ == filteretype:
       
   503                     entities.append(entity)
       
   504         return entities
       
   505 
       
   506 # XXX should be a view usable using uicfg
       
   507 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent):
       
   508     """base class for boxes which let add or remove entities linked by a given
       
   509     relation
       
   510 
       
   511     subclasses should define at least id, rtype and target class attributes.
       
   512     """
       
   513     def render_title(self, w):
       
   514         w(display_name(self._cw, self.rtype, role(self),
       
   515                        context=self.entity.__regid__))
       
   516 
       
   517     def render_body(self, w):
       
   518         self._cw.add_js('cubicweb.ajax.js')
       
   519         related = self.related_boxitems(self.entity)
       
   520         unrelated = self.unrelated_boxitems(self.entity)
       
   521         self.items.extend(related)
       
   522         if related and unrelated:
       
   523             self.items.append(u'<hr class="boxSeparator"/>')
       
   524         self.items.extend(unrelated)
       
   525         self.render_items(w)
       
   526 
       
   527 
       
   528 class AjaxEditRelationCtxComponent(EntityCtxComponent):
       
   529     __select__ = EntityCtxComponent.__select__ & (
       
   530         partial_relation_possible(action='add') | partial_has_related_entities())
       
   531 
       
   532     # view used to display related entties
       
   533     item_vid = 'incontext'
       
   534     # values separator when multiple values are allowed
       
   535     separator = ','
       
   536     # msgid of the message to display when some new relation has been added/removed
       
   537     added_msg = None
       
   538     removed_msg = None
       
   539 
       
   540     # class attributes below *must* be set in concret classes (additionaly to
       
   541     # rtype / role [/ target_etype]. They should correspond to js_* methods on
       
   542     # the json controller
       
   543 
       
   544     # function(eid)
       
   545     # -> expected to return a list of values to display as input selector
       
   546     #    vocabulary
       
   547     fname_vocabulary = None
       
   548 
       
   549     # function(eid, value)
       
   550     # -> handle the selector's input (eg create necessary entities and/or
       
   551     # relations). If the relation is multiple, you'll get a list of value, else
       
   552     # a single string value.
       
   553     fname_validate = None
       
   554 
       
   555     # function(eid, linked entity eid)
       
   556     # -> remove the relation
       
   557     fname_remove = None
       
   558 
       
   559     def __init__(self, *args, **kwargs):
       
   560         super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs)
       
   561         self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
       
   562 
       
   563     def render_title(self, w):
       
   564         w(self.rdef.rtype.display_name(self._cw, self.role,
       
   565                                        context=self.entity.__regid__))
       
   566 
       
   567     def render_body(self, w):
       
   568         req = self._cw
       
   569         entity = self.entity
       
   570         related = entity.related(self.rtype, self.role)
       
   571         if self.role == 'subject':
       
   572             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
       
   573             maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
       
   574         else:
       
   575             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
       
   576             maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
       
   577         if mayadd or maydel:
       
   578             req.add_js(('jquery.ui.js', 'cubicweb.widgets.js'))
       
   579             req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
       
   580             req.add_css('jquery.ui.css')
       
   581         _ = req._
       
   582         if related:
       
   583             w(u'<table class="ajaxEditRelationTable">')
       
   584             for rentity in related.entities():
       
   585                 # for each related entity, provide a link to remove the relation
       
   586                 subview = rentity.view(self.item_vid)
       
   587                 if maydel:
       
   588                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
       
   589                         self.__regid__, entity.eid, rentity.eid,
       
   590                         self.fname_remove,
       
   591                         self.removed_msg and _(self.removed_msg)))
       
   592                     w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>'
       
   593                       '<td class="entity"> %s</td></tr>' % (xml_escape(jscall),
       
   594                                                             subview))
       
   595                 else:
       
   596                     w(u'<tr><td class="entity">%s</td></tr>' % (subview))
       
   597             w(u'</table>')
       
   598         else:
       
   599             w(_('no related entity'))
       
   600         if mayadd:
       
   601             multiple = self.rdef.role_cardinality(self.role) in '*+'
       
   602             w(u'<table><tr><td>')
       
   603             jscall = unicode(js.ajaxBoxShowSelector(
       
   604                 self.__regid__, entity.eid, self.fname_vocabulary,
       
   605                 self.fname_validate, self.added_msg and _(self.added_msg),
       
   606                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
       
   607                 multiple and self.separator))
       
   608             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
       
   609                 xml_escape(jscall),
       
   610                 multiple and _('add_relation') or _('update_relation')))
       
   611             w(u'</td><td>')
       
   612             w(u'<div id="%sHolder"></div>' % self.domid)
       
   613             w(u'</td></tr></table>')
       
   614 
       
   615 
       
   616 class RelatedObjectsCtxComponent(EntityCtxComponent):
       
   617     """a contextual component to display entities related to another"""
       
   618     __select__ = EntityCtxComponent.__select__ & partial_has_related_entities()
       
   619     context = 'navcontentbottom'
       
   620     rtype = None
       
   621     role = 'subject'
       
   622 
       
   623     vid = 'list'
       
   624 
       
   625     def render_body(self, w):
       
   626         rset = self.entity.related(self.rtype, role(self))
       
   627         self._cw.view(self.vid, rset, w=w)
       
   628 
       
   629 
       
   630 # old contextual components, deprecated ########################################
       
   631 
       
   632 class EntityVComponent(Component):
       
   633     """abstract base class for additinal components displayed in content
       
   634     headers and footer according to:
       
   635 
       
   636     * the displayed entity's type
       
   637     * a context (currently 'header' or 'footer')
       
   638 
       
   639     it should be configured using .accepts, .etype, .rtype, .target and
       
   640     .context class attributes
       
   641     """
       
   642     __metaclass__ = class_deprecated
       
   643     __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'
       
   644 
       
   645     __registry__ = 'ctxcomponents'
       
   646     __select__ = one_line_rset()
       
   647 
       
   648     cw_property_defs = {
       
   649         _('visible'):  dict(type='Boolean', default=True,
       
   650                             help=_('display the component or not')),
       
   651         _('order'):    dict(type='Int', default=99,
       
   652                             help=_('display order of the component')),
       
   653         _('context'):  dict(type='String', default='navtop',
       
   654                             vocabulary=(_('navtop'), _('navbottom'),
       
   655                                         _('navcontenttop'), _('navcontentbottom'),
       
   656                                         _('ctxtoolbar')),
       
   657                             help=_('context where this component should be displayed')),
       
   658     }
       
   659 
       
   660     context = 'navcontentbottom'
       
   661 
       
   662     def call(self, view=None):
       
   663         if self.cw_rset is None:
       
   664             self.entity_call(self.cw_extra_kwargs.pop('entity'))
       
   665         else:
       
   666             self.cell_call(0, 0, view=view)
       
   667 
       
   668     def cell_call(self, row, col, view=None):
       
   669         self.entity_call(self.cw_rset.get_entity(row, col), view=view)
       
   670 
       
   671     def entity_call(self, entity, view=None):
       
   672         raise NotImplementedError()
   184 
   673 
   185 
   674 
   186 class RelatedObjectsVComponent(EntityVComponent):
   675 class RelatedObjectsVComponent(EntityVComponent):
   187     """a section to display some related entities"""
   676     """a section to display some related entities"""
   188     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
   677     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
   201         else:
   690         else:
   202             eid = self.cw_rset[row][col]
   691             eid = self.cw_rset[row][col]
   203             rset = self._cw.execute(self.rql(), {'x': eid})
   692             rset = self._cw.execute(self.rql(), {'x': eid})
   204         if not rset.rowcount:
   693         if not rset.rowcount:
   205             return
   694             return
   206         self.w(u'<div class="%s">' % self.div_class())
   695         self.w(u'<div class="%s">' % self.cssclass)
   207         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
   696         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
   208         self.wview(self.vid, rset)
   697         self.wview(self.vid, rset)
   209         self.w(u'</div>')
   698         self.w(u'</div>')
   210 
   699 
   211 
   700 
       
   701 
   212 VComponent = class_renamed('VComponent', Component,
   702 VComponent = class_renamed('VComponent', Component,
   213                            'VComponent is deprecated, use Component')
   703                            '[3.2] VComponent is deprecated, use Component')
   214 SingletonVComponent = class_renamed('SingletonVComponent', Component,
   704 SingletonVComponent = class_renamed('SingletonVComponent', Component,
   215                                     'SingletonVComponent is deprecated, use '
   705                                     '[3.2] SingletonVComponent is deprecated, use '
   216                                     'Component and explicit registration control')
   706                                     'Component and explicit registration control')