web/component.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """abstract component class and base components definition for CubicWeb web
       
    19 client
       
    20 """
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 from cubicweb import _
       
    24 
       
    25 from warnings import warn
       
    26 
       
    27 from six import PY3, add_metaclass, text_type
       
    28 
       
    29 from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
       
    30 from logilab.mtconverter import xml_escape
       
    31 
       
    32 from cubicweb import Unauthorized, role, target, tags
       
    33 from cubicweb.schema import display_name
       
    34 from cubicweb.uilib import js, domid
       
    35 from cubicweb.utils import json_dumps, js_href
       
    36 from cubicweb.view import ReloadableMixIn, Component
       
    37 from cubicweb.predicates import (no_cnx, paginated_rset, one_line_rset,
       
    38                                 non_final_entity, partial_relation_possible,
       
    39                                 partial_has_related_entities)
       
    40 from cubicweb.appobject import AppObject
       
    41 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
       
    42 
       
    43 
       
    44 # abstract base class for navigation components ################################
       
    45 
       
    46 class NavigationComponent(Component):
       
    47     """abstract base class for navigation components"""
       
    48     __regid__ = 'navigation'
       
    49     __select__ = paginated_rset()
       
    50 
       
    51     cw_property_defs = {
       
    52         _('visible'):  dict(type='Boolean', default=True,
       
    53                             help=_('display the component or not')),
       
    54         }
       
    55 
       
    56     page_size_property = 'navigation.page-size'
       
    57     start_param = '__start'
       
    58     stop_param = '__stop'
       
    59     page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>'
       
    60     selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'
       
    61     previous_page_link_templ = next_page_link_templ = page_link_templ
       
    62 
       
    63     def __init__(self, req, rset, **kwargs):
       
    64         super(NavigationComponent, self).__init__(req, rset=rset, **kwargs)
       
    65         self.starting_from = 0
       
    66         self.total = rset.rowcount
       
    67 
       
    68     def get_page_size(self):
       
    69         try:
       
    70             return self._page_size
       
    71         except AttributeError:
       
    72             page_size = self.cw_extra_kwargs.get('page_size')
       
    73             if page_size is None:
       
    74                 if 'page_size' in self._cw.form:
       
    75                     page_size = int(self._cw.form['page_size'])
       
    76                 else:
       
    77                     page_size = self._cw.property_value(self.page_size_property)
       
    78             self._page_size = page_size
       
    79             return page_size
       
    80 
       
    81     def set_page_size(self, page_size):
       
    82         self._page_size = page_size
       
    83 
       
    84     page_size = property(get_page_size, set_page_size)
       
    85 
       
    86     def page_boundaries(self):
       
    87         try:
       
    88             stop = int(self._cw.form[self.stop_param]) + 1
       
    89             start = int(self._cw.form[self.start_param])
       
    90         except KeyError:
       
    91             start, stop = 0, self.page_size
       
    92         if start >= len(self.cw_rset):
       
    93             start, stop = 0, self.page_size
       
    94         self.starting_from = start
       
    95         return start, stop
       
    96 
       
    97     def clean_params(self, params):
       
    98         if self.start_param in params:
       
    99             del params[self.start_param]
       
   100         if self.stop_param in params:
       
   101             del params[self.stop_param]
       
   102 
       
   103     def page_url(self, path, params, start=None, stop=None):
       
   104         params = dict(params)
       
   105         params['__fromnavigation'] = 1
       
   106         if start is not None:
       
   107             params[self.start_param] = start
       
   108         if stop is not None:
       
   109             params[self.stop_param] = stop
       
   110         view = self.cw_extra_kwargs.get('view')
       
   111         if view is not None and hasattr(view, 'page_navigation_url'):
       
   112             url = view.page_navigation_url(self, path, params)
       
   113         elif path in ('json', 'ajax'):
       
   114             # 'ajax' is the new correct controller, but the old 'json'
       
   115             # controller should still be supported
       
   116             url = self.ajax_page_url(**params)
       
   117         else:
       
   118             url = self._cw.build_url(path, **params)
       
   119         # XXX hack to avoid opening a new page containing the evaluation of the
       
   120         # js expression on ajax call
       
   121         if url.startswith('javascript:'):
       
   122             url += '; $.noop();'
       
   123         return url
       
   124 
       
   125     def ajax_page_url(self, **params):
       
   126         divid = params.setdefault('divid', 'pageContent')
       
   127         params['rql'] = self.cw_rset.printable_rql()
       
   128         return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % (
       
   129             json_dumps('#'+divid), js.ajaxFuncArgs('view', params)))
       
   130 
       
   131     def page_link(self, path, params, start, stop, content):
       
   132         url = xml_escape(self.page_url(path, params, start, stop))
       
   133         if start == self.starting_from:
       
   134             return self.selected_page_link_templ % (url, content, content)
       
   135         return self.page_link_templ % (url, content, content)
       
   136 
       
   137     @property
       
   138     def prev_icon_url(self):
       
   139         return xml_escape(self._cw.data_url('go_prev.png'))
       
   140 
       
   141     @property
       
   142     def next_icon_url(self):
       
   143         return xml_escape(self._cw.data_url('go_next.png'))
       
   144 
       
   145     @property
       
   146     def no_previous_page_link(self):
       
   147         return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
       
   148                 (self.prev_icon_url, self._cw._('there is no previous page')))
       
   149 
       
   150     @property
       
   151     def no_next_page_link(self):
       
   152         return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
       
   153                 (self.next_icon_url, self._cw._('there is no next page')))
       
   154 
       
   155     @property
       
   156     def no_content_prev_link(self):
       
   157         return (u'<img src="%s" alt="%s" class="prevnext"/>' % (
       
   158                 (self.prev_icon_url, self._cw._('no content prev link'))))
       
   159 
       
   160     @property
       
   161     def no_content_next_link(self):
       
   162         return (u'<img src="%s" alt="%s" class="prevnext"/>' %
       
   163                 (self.next_icon_url, self._cw._('no content next link')))
       
   164 
       
   165     def previous_link(self, path, params, content=None, title=_('previous_results')):
       
   166         if not content:
       
   167             content = self.no_content_prev_link
       
   168         start = self.starting_from
       
   169         if not start :
       
   170             return self.no_previous_page_link
       
   171         start = max(0, start - self.page_size)
       
   172         stop = start + self.page_size - 1
       
   173         url = xml_escape(self.page_url(path, params, start, stop))
       
   174         return self.previous_page_link_templ % (url, self._cw._(title), content)
       
   175 
       
   176     def next_link(self, path, params, content=None, title=_('next_results')):
       
   177         if not content:
       
   178             content = self.no_content_next_link
       
   179         start = self.starting_from + self.page_size
       
   180         if start >= self.total:
       
   181             return self.no_next_page_link
       
   182         stop = start + self.page_size - 1
       
   183         url = xml_escape(self.page_url(path, params, start, stop))
       
   184         return self.next_page_link_templ % (url, self._cw._(title), content)
       
   185 
       
   186 
       
   187 # new contextual components system #############################################
       
   188 
       
   189 def override_ctx(cls, **kwargs):
       
   190     cwpdefs = cls.cw_property_defs.copy()
       
   191     cwpdefs['context']  = cwpdefs['context'].copy()
       
   192     cwpdefs['context'].update(kwargs)
       
   193     return cwpdefs
       
   194 
       
   195 
       
   196 class EmptyComponent(Exception):
       
   197     """some selectable component has actually no content and should not be
       
   198     rendered
       
   199     """
       
   200 
       
   201 
       
   202 class Link(object):
       
   203     """a link to a view or action in the ui.
       
   204 
       
   205     Use this rather than `cw.web.htmlwidgets.BoxLink`.
       
   206 
       
   207     Note this class could probably be avoided with a proper DOM on the server
       
   208     side.
       
   209     """
       
   210     newstyle = True
       
   211 
       
   212     def __init__(self, href, label, **attrs):
       
   213         self.href = href
       
   214         self.label = label
       
   215         self.attrs = attrs
       
   216 
       
   217     def __unicode__(self):
       
   218         return tags.a(self.label, href=self.href, **self.attrs)
       
   219 
       
   220     if PY3:
       
   221         __str__ = __unicode__
       
   222 
       
   223     def render(self, w):
       
   224         w(tags.a(self.label, href=self.href, **self.attrs))
       
   225 
       
   226     def __repr__(self):
       
   227         return '<%s: href=%r label=%r %r>' % (self.__class__.__name__,
       
   228                                               self.href, self.label, self.attrs)
       
   229 
       
   230 
       
   231 class Separator(object):
       
   232     """a menu separator.
       
   233 
       
   234     Use this rather than `cw.web.htmlwidgets.BoxSeparator`.
       
   235     """
       
   236     newstyle = True
       
   237 
       
   238     def render(self, w):
       
   239         w(u'<hr class="boxSeparator"/>')
       
   240 
       
   241 
       
   242 def _bwcompatible_render_item(w, item):
       
   243     if hasattr(item, 'render'):
       
   244         if getattr(item, 'newstyle', False):
       
   245             if isinstance(item, Separator):
       
   246                 w(u'</ul>')
       
   247                 item.render(w)
       
   248                 w(u'<ul>')
       
   249             else:
       
   250                 w(u'<li>')
       
   251                 item.render(w)
       
   252                 w(u'</li>')
       
   253         else:
       
   254             item.render(w) # XXX displays <li> by itself
       
   255     else:
       
   256         w(u'<li>%s</li>' % item)
       
   257 
       
   258 
       
   259 class Layout(Component):
       
   260     __regid__ = 'component_layout'
       
   261     __abstract__ = True
       
   262 
       
   263     def init_rendering(self):
       
   264         """init view for rendering. Return true if we should go on, false
       
   265         if we should stop now.
       
   266         """
       
   267         view = self.cw_extra_kwargs['view']
       
   268         try:
       
   269             view.init_rendering()
       
   270         except Unauthorized as ex:
       
   271             self.warning("can't render %s: %s", view, ex)
       
   272             return False
       
   273         except EmptyComponent:
       
   274             return False
       
   275         return True
       
   276 
       
   277 
       
   278 class LayoutableMixIn(object):
       
   279     layout_id = None # to be defined in concret class
       
   280     layout_args = {}
       
   281 
       
   282     def layout_render(self, w, **kwargs):
       
   283         getlayout = self._cw.vreg['components'].select
       
   284         layout = getlayout(self.layout_id, self._cw, **self.layout_select_args())
       
   285         layout.render(w)
       
   286 
       
   287     def layout_select_args(self):
       
   288         args  = dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col,
       
   289                      view=self)
       
   290         args.update(self.layout_args)
       
   291         return args
       
   292 
       
   293 
       
   294 class CtxComponent(LayoutableMixIn, AppObject):
       
   295     """base class for contextual components. The following contexts are
       
   296     predefined:
       
   297 
       
   298     * boxes: 'left', 'incontext', 'right'
       
   299     * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom'
       
   300     * other: 'ctxtoolbar'
       
   301 
       
   302     The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar'
       
   303     contexts are handled by the default primary view, others by the default main
       
   304     template.
       
   305 
       
   306     All subclasses may not support all those contexts (for instance if it can't
       
   307     be displayed as box, or as a toolbar icon). You may restrict allowed context
       
   308     as follows:
       
   309 
       
   310     .. sourcecode:: python
       
   311 
       
   312       class MyComponent(CtxComponent):
       
   313           cw_property_defs = override_ctx(CtxComponent,
       
   314                                           vocabulary=[list of contexts])
       
   315           context = 'my default context'
       
   316 
       
   317     You can configure a component's default context by simply giving an
       
   318     appropriate value to the `context` class attribute, as seen above.
       
   319     """
       
   320     __registry__ = 'ctxcomponents'
       
   321     __select__ = ~no_cnx()
       
   322 
       
   323     categories_in_order = ()
       
   324     cw_property_defs = {
       
   325         _('visible'): dict(type='Boolean', default=True,
       
   326                            help=_('display the box or not')),
       
   327         _('order'):   dict(type='Int', default=99,
       
   328                            help=_('display order of the box')),
       
   329         _('context'): dict(type='String', default='left',
       
   330                            vocabulary=(_('left'), _('incontext'), _('right'),
       
   331                                        _('navtop'), _('navbottom'),
       
   332                                        _('navcontenttop'), _('navcontentbottom'),
       
   333                                        _('ctxtoolbar')),
       
   334                            help=_('context where this component should be displayed')),
       
   335         }
       
   336     visible = True
       
   337     order = 0
       
   338     context = 'left'
       
   339     contextual = False
       
   340     title = None
       
   341     layout_id = 'component_layout'
       
   342 
       
   343     def render(self, w, **kwargs):
       
   344         self.layout_render(w, **kwargs)
       
   345 
       
   346     def layout_select_args(self):
       
   347         args = super(CtxComponent, self).layout_select_args()
       
   348         try:
       
   349             # XXX ensure context is given when the component is reloaded through
       
   350             # ajax
       
   351             args['context'] = self.cw_extra_kwargs['context']
       
   352         except KeyError:
       
   353             args['context'] = self.cw_propval('context')
       
   354         return args
       
   355 
       
   356     def init_rendering(self):
       
   357         """init rendering callback: that's the good time to check your component
       
   358         has some content to display. If not, you can still raise
       
   359         :exc:`EmptyComponent` to inform it should be skipped.
       
   360 
       
   361         Also, :exc:`Unauthorized` will be caught, logged, then the component
       
   362         will be skipped.
       
   363         """
       
   364         self.items = []
       
   365 
       
   366     @property
       
   367     def domid(self):
       
   368         """return the HTML DOM identifier for this component"""
       
   369         return domid(self.__regid__)
       
   370 
       
   371     @property
       
   372     def cssclass(self):
       
   373         """return the CSS class name for this component"""
       
   374         return domid(self.__regid__)
       
   375 
       
   376     def render_title(self, w):
       
   377         """return the title for this component"""
       
   378         if self.title:
       
   379             w(self._cw._(self.title))
       
   380 
       
   381     def render_body(self, w):
       
   382         """return the body (content) for this component"""
       
   383         raise NotImplementedError()
       
   384 
       
   385     def render_items(self, w, items=None, klass=u'boxListing'):
       
   386         if items is None:
       
   387             items = self.items
       
   388         assert items
       
   389         w(u'<ul class="%s">' % klass)
       
   390         for item in items:
       
   391             _bwcompatible_render_item(w, item)
       
   392         w(u'</ul>')
       
   393 
       
   394     def append(self, item):
       
   395         self.items.append(item)
       
   396 
       
   397     def action_link(self, action):
       
   398         return self.link(self._cw._(action.title), action.url())
       
   399 
       
   400     def link(self, title, url, **kwargs):
       
   401         if self._cw.selected(url):
       
   402             try:
       
   403                 kwargs['klass'] += ' selected'
       
   404             except KeyError:
       
   405                 kwargs['klass'] = 'selected'
       
   406         return Link(url, title, **kwargs)
       
   407 
       
   408     def separator(self):
       
   409         return Separator()
       
   410 
       
   411 
       
   412 class EntityCtxComponent(CtxComponent):
       
   413     """base class for boxes related to a single entity"""
       
   414     __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset()
       
   415     context = 'incontext'
       
   416     contextual = True
       
   417 
       
   418     def __init__(self, *args, **kwargs):
       
   419         super(EntityCtxComponent, self).__init__(*args, **kwargs)
       
   420         try:
       
   421             entity = kwargs['entity']
       
   422         except KeyError:
       
   423             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   424         self.entity = entity
       
   425 
       
   426     def layout_select_args(self):
       
   427         args = super(EntityCtxComponent, self).layout_select_args()
       
   428         args['entity'] = self.entity
       
   429         return args
       
   430 
       
   431     @property
       
   432     def domid(self):
       
   433         return domid(self.__regid__) + text_type(self.entity.eid)
       
   434 
       
   435     def lazy_view_holder(self, w, entity, oid, registry='views'):
       
   436         """add a holder and return a URL that may be used to replace this
       
   437         holder by the html generate by the view specified by registry and
       
   438         identifier. Registry defaults to 'views'.
       
   439         """
       
   440         holderid = '%sHolder' % self.domid
       
   441         w(u'<div id="%s"></div>' % holderid)
       
   442         params = self.cw_extra_kwargs.copy()
       
   443         params.pop('view', None)
       
   444         params.pop('entity', None)
       
   445         form = params.pop('formparams', {})
       
   446         if entity.has_eid():
       
   447             eid = entity.eid
       
   448         else:
       
   449             eid = None
       
   450             form['etype'] = entity.cw_etype
       
   451             form['tempEid'] = entity.eid
       
   452         args = [json_dumps(x) for x in (registry, oid, eid, params)]
       
   453         return self._cw.ajax_replace_url(
       
   454             holderid, fname='render', arg=args, **form)
       
   455 
       
   456 
       
   457 # high level abstract classes ##################################################
       
   458 
       
   459 class RQLCtxComponent(CtxComponent):
       
   460     """abstract box for boxes displaying the content of a rql query not related
       
   461     to the current result set.
       
   462 
       
   463     Notice that this class's init_rendering implemention is overwriting context
       
   464     result set (eg `cw_rset`) with the result set returned by execution of
       
   465     `to_display_rql()`.
       
   466     """
       
   467     rql = None
       
   468 
       
   469     def to_display_rql(self):
       
   470         """return arguments to give to self._cw.execute, as a tuple, to build
       
   471         the result set to be displayed by this box.
       
   472         """
       
   473         assert self.rql is not None, self.__regid__
       
   474         return (self.rql,)
       
   475 
       
   476     def init_rendering(self):
       
   477         super(RQLCtxComponent, self).init_rendering()
       
   478         self.cw_rset = self._cw.execute(*self.to_display_rql())
       
   479         if not self.cw_rset:
       
   480             raise EmptyComponent()
       
   481 
       
   482     def render_body(self, w):
       
   483         rset = self.cw_rset
       
   484         if len(rset[0]) == 2:
       
   485             items = []
       
   486             for i, (eid, label) in enumerate(rset):
       
   487                 entity = rset.get_entity(i, 0)
       
   488                 items.append(self.link(label, entity.absolute_url()))
       
   489         else:
       
   490             items = [self.link(e.dc_title(), e.absolute_url())
       
   491                      for e in rset.entities()]
       
   492         self.render_items(w, items)
       
   493 
       
   494 
       
   495 class EditRelationMixIn(ReloadableMixIn):
       
   496 
       
   497     def box_item(self, entity, etarget, fname, label):
       
   498         """builds HTML link to edit relation between `entity` and `etarget`"""
       
   499         args = {role(self) : entity.eid, target(self): etarget.eid}
       
   500         # for each target, provide a link to edit the relation
       
   501         jscall = js.cw.utils.callAjaxFuncThenReload(fname,
       
   502                                                     self.rtype,
       
   503                                                     args['subject'],
       
   504                                                     args['object'])
       
   505         return u'[<a href="javascript: %s" class="action">%s</a>] %s' % (
       
   506             xml_escape(text_type(jscall)), label, etarget.view('incontext'))
       
   507 
       
   508     def related_boxitems(self, entity):
       
   509         return [self.box_item(entity, etarget, 'delete_relation', u'-')
       
   510                 for etarget in self.related_entities(entity)]
       
   511 
       
   512     def related_entities(self, entity):
       
   513         return entity.related(self.rtype, role(self), entities=True)
       
   514 
       
   515     def unrelated_boxitems(self, entity):
       
   516         return [self.box_item(entity, etarget, 'add_relation', u'+')
       
   517                 for etarget in self.unrelated_entities(entity)]
       
   518 
       
   519     def unrelated_entities(self, entity):
       
   520         """returns the list of unrelated entities, using the entity's
       
   521         appropriate vocabulary function
       
   522         """
       
   523         skip = set(text_type(e.eid) for e in entity.related(self.rtype, role(self),
       
   524                                                           entities=True))
       
   525         skip.add(None)
       
   526         skip.add(INTERNAL_FIELD_VALUE)
       
   527         filteretype = getattr(self, 'etype', None)
       
   528         entities = []
       
   529         form = self._cw.vreg['forms'].select('edition', self._cw,
       
   530                                              rset=self.cw_rset,
       
   531                                              row=self.cw_row or 0)
       
   532         field = form.field_by_name(self.rtype, role(self), entity.e_schema)
       
   533         for _, eid in field.vocabulary(form):
       
   534             if eid not in skip:
       
   535                 entity = self._cw.entity_from_eid(eid)
       
   536                 if filteretype is None or entity.cw_etype == filteretype:
       
   537                     entities.append(entity)
       
   538         return entities
       
   539 
       
   540 # XXX should be a view usable using uicfg
       
   541 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent):
       
   542     """base class for boxes which let add or remove entities linked by a given
       
   543     relation
       
   544 
       
   545     subclasses should define at least id, rtype and target class attributes.
       
   546     """
       
   547     # to be defined in concrete classes
       
   548     rtype = None
       
   549 
       
   550     def render_title(self, w):
       
   551         w(display_name(self._cw, self.rtype, role(self),
       
   552                        context=self.entity.cw_etype))
       
   553 
       
   554     def render_body(self, w):
       
   555         self._cw.add_js('cubicweb.ajax.js')
       
   556         related = self.related_boxitems(self.entity)
       
   557         unrelated = self.unrelated_boxitems(self.entity)
       
   558         self.items.extend(related)
       
   559         if related and unrelated:
       
   560             self.items.append(u'<hr class="boxSeparator"/>')
       
   561         self.items.extend(unrelated)
       
   562         self.render_items(w)
       
   563 
       
   564 
       
   565 class AjaxEditRelationCtxComponent(EntityCtxComponent):
       
   566     __select__ = EntityCtxComponent.__select__ & (
       
   567         partial_relation_possible(action='add') | partial_has_related_entities())
       
   568 
       
   569     # view used to display related entties
       
   570     item_vid = 'incontext'
       
   571     # values separator when multiple values are allowed
       
   572     separator = ','
       
   573     # msgid of the message to display when some new relation has been added/removed
       
   574     added_msg = None
       
   575     removed_msg = None
       
   576 
       
   577     # to be defined in concrete classes
       
   578     rtype = role = target_etype = None
       
   579     # class attributes below *must* be set in concrete classes (additionally to
       
   580     # rtype / role [/ target_etype]. They should correspond to js_* methods on
       
   581     # the json controller
       
   582 
       
   583     # function(eid)
       
   584     # -> expected to return a list of values to display as input selector
       
   585     #    vocabulary
       
   586     fname_vocabulary = None
       
   587 
       
   588     # function(eid, value)
       
   589     # -> handle the selector's input (eg create necessary entities and/or
       
   590     # relations). If the relation is multiple, you'll get a list of value, else
       
   591     # a single string value.
       
   592     fname_validate = None
       
   593 
       
   594     # function(eid, linked entity eid)
       
   595     # -> remove the relation
       
   596     fname_remove = None
       
   597 
       
   598     def __init__(self, *args, **kwargs):
       
   599         super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs)
       
   600         self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
       
   601 
       
   602     def render_title(self, w):
       
   603         w(self.rdef.rtype.display_name(self._cw, self.role,
       
   604                                        context=self.entity.cw_etype))
       
   605 
       
   606     def add_js_css(self):
       
   607         self._cw.add_js(('jquery.ui.js', 'cubicweb.widgets.js'))
       
   608         self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
       
   609         self._cw.add_css('jquery.ui.css')
       
   610         return True
       
   611 
       
   612     def render_body(self, w):
       
   613         req = self._cw
       
   614         entity = self.entity
       
   615         related = entity.related(self.rtype, self.role)
       
   616         if self.role == 'subject':
       
   617             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
       
   618         else:
       
   619             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
       
   620         js_css_added = False
       
   621         if mayadd:
       
   622             js_css_added = self.add_js_css()
       
   623         _ = req._
       
   624         if related:
       
   625             maydel = None
       
   626             w(u'<table class="ajaxEditRelationTable">')
       
   627             for rentity in related.entities():
       
   628                 if maydel is None:
       
   629                     # Only check permission for the first related.
       
   630                     if self.role == 'subject':
       
   631                         fromeid, toeid = entity.eid, rentity.eid
       
   632                     else:
       
   633                         fromeid, toeid = rentity.eid, entity.eid
       
   634                     maydel = self.rdef.has_perm(
       
   635                             req, 'delete', fromeid=fromeid, toeid=toeid)
       
   636                 # for each related entity, provide a link to remove the relation
       
   637                 subview = rentity.view(self.item_vid)
       
   638                 if maydel:
       
   639                     if not js_css_added:
       
   640                         js_css_added = self.add_js_css()
       
   641                     jscall = text_type(js.ajaxBoxRemoveLinkedEntity(
       
   642                         self.__regid__, entity.eid, rentity.eid,
       
   643                         self.fname_remove,
       
   644                         self.removed_msg and _(self.removed_msg)))
       
   645                     w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>'
       
   646                       '<td class="entity"> %s</td></tr>' % (xml_escape(jscall),
       
   647                                                             subview))
       
   648                 else:
       
   649                     w(u'<tr><td class="entity">%s</td></tr>' % (subview))
       
   650             w(u'</table>')
       
   651         else:
       
   652             w(_('no related entity'))
       
   653         if mayadd:
       
   654             multiple = self.rdef.role_cardinality(self.role) in '*+'
       
   655             w(u'<table><tr><td>')
       
   656             jscall = text_type(js.ajaxBoxShowSelector(
       
   657                 self.__regid__, entity.eid, self.fname_vocabulary,
       
   658                 self.fname_validate, self.added_msg and _(self.added_msg),
       
   659                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
       
   660                 multiple and self.separator))
       
   661             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
       
   662                 xml_escape(jscall),
       
   663                 multiple and _('add_relation') or _('update_relation')))
       
   664             w(u'</td><td>')
       
   665             w(u'<div id="%sHolder"></div>' % self.domid)
       
   666             w(u'</td></tr></table>')
       
   667 
       
   668 
       
   669 class RelatedObjectsCtxComponent(EntityCtxComponent):
       
   670     """a contextual component to display entities related to another"""
       
   671     __select__ = EntityCtxComponent.__select__ & partial_has_related_entities()
       
   672     context = 'navcontentbottom'
       
   673     rtype = None
       
   674     role = 'subject'
       
   675 
       
   676     vid = 'list'
       
   677 
       
   678     def render_body(self, w):
       
   679         rset = self.entity.related(self.rtype, role(self))
       
   680         self._cw.view(self.vid, rset, w=w)
       
   681 
       
   682 
       
   683 # old contextual components, deprecated ########################################
       
   684 
       
   685 @add_metaclass(class_deprecated)
       
   686 class EntityVComponent(Component):
       
   687     """abstract base class for additinal components displayed in content
       
   688     headers and footer according to:
       
   689 
       
   690     * the displayed entity's type
       
   691     * a context (currently 'header' or 'footer')
       
   692 
       
   693     it should be configured using .accepts, .etype, .rtype, .target and
       
   694     .context class attributes
       
   695     """
       
   696     __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'
       
   697 
       
   698     __registry__ = 'ctxcomponents'
       
   699     __select__ = one_line_rset()
       
   700 
       
   701     cw_property_defs = {
       
   702         _('visible'):  dict(type='Boolean', default=True,
       
   703                             help=_('display the component or not')),
       
   704         _('order'):    dict(type='Int', default=99,
       
   705                             help=_('display order of the component')),
       
   706         _('context'):  dict(type='String', default='navtop',
       
   707                             vocabulary=(_('navtop'), _('navbottom'),
       
   708                                         _('navcontenttop'), _('navcontentbottom'),
       
   709                                         _('ctxtoolbar')),
       
   710                             help=_('context where this component should be displayed')),
       
   711     }
       
   712 
       
   713     context = 'navcontentbottom'
       
   714 
       
   715     def call(self, view=None):
       
   716         if self.cw_rset is None:
       
   717             self.entity_call(self.cw_extra_kwargs.pop('entity'))
       
   718         else:
       
   719             self.cell_call(0, 0, view=view)
       
   720 
       
   721     def cell_call(self, row, col, view=None):
       
   722         self.entity_call(self.cw_rset.get_entity(row, col), view=view)
       
   723 
       
   724     def entity_call(self, entity, view=None):
       
   725         raise NotImplementedError()
       
   726 
       
   727 class RelatedObjectsVComponent(EntityVComponent):
       
   728     """a section to display some related entities"""
       
   729     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
       
   730 
       
   731     vid = 'list'
       
   732     # to be defined in concrete classes
       
   733     rtype = title = None
       
   734 
       
   735     def rql(self):
       
   736         """override this method if you want to use a custom rql query"""
       
   737         return None
       
   738 
       
   739     def cell_call(self, row, col, view=None):
       
   740         rql = self.rql()
       
   741         if rql is None:
       
   742             entity = self.cw_rset.get_entity(row, col)
       
   743             rset = entity.related(self.rtype, role(self))
       
   744         else:
       
   745             eid = self.cw_rset[row][col]
       
   746             rset = self._cw.execute(self.rql(), {'x': eid})
       
   747         if not rset.rowcount:
       
   748             return
       
   749         self.w(u'<div class="%s">' % self.cssclass)
       
   750         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
       
   751         self.wview(self.vid, rset)
       
   752         self.w(u'</div>')