web/component.py
changeset 6141 b8287e54b528
parent 6140 65a619eb31c4
child 6151 f910c60b84ff
equal deleted inserted replaced
6140:65a619eb31c4 6141:b8287e54b528
    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 logilab.common.deprecation import class_deprecated, class_renamed
    26 from logilab.mtconverter import xml_escape
    26 from logilab.mtconverter import xml_escape
    27 
    27 
    28 from cubicweb import role
    28 from cubicweb import Unauthorized, role, tags
    29 from cubicweb.utils import json_dumps
    29 from cubicweb.uilib import js, domid
    30 from cubicweb.view import Component
    30 from cubicweb.view import ReloadableMixIn, Component
    31 from cubicweb.selectors import (
    31 from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
    32     paginated_rset, one_line_rset, primary_view, match_context_prop,
    32                                 non_final_entity, partial_relation_possible,
    33     partial_has_related_entities)
    33                                 partial_has_related_entities)
    34 
    34 from cubicweb.appobject import AppObject
    35 
    35 from cubicweb.web import htmlwidgets, stdmsgs
    36 class EntityVComponent(Component):
    36 
    37     """abstract base class for additinal components displayed in content
    37 
    38     headers and footer according to:
    38 # abstract base class for navigation components ################################
    39 
       
    40     * the displayed entity's type
       
    41     * a context (currently 'header' or 'footer')
       
    42 
       
    43     it should be configured using .accepts, .etype, .rtype, .target and
       
    44     .context class attributes
       
    45     """
       
    46 
       
    47     __registry__ = 'contentnavigation'
       
    48     __select__ = one_line_rset() & primary_view() & match_context_prop()
       
    49 
       
    50     cw_property_defs = {
       
    51         _('visible'):  dict(type='Boolean', default=True,
       
    52                             help=_('display the component or not')),
       
    53         _('order'):    dict(type='Int', default=99,
       
    54                             help=_('display order of the component')),
       
    55         _('context'):  dict(type='String', default='navtop',
       
    56                             vocabulary=(_('navtop'), _('navbottom'),
       
    57                                         _('navcontenttop'), _('navcontentbottom'),
       
    58                                         _('ctxtoolbar')),
       
    59                             help=_('context where this component should be displayed')),
       
    60     }
       
    61 
       
    62     context = 'navcontentbottom'
       
    63 
       
    64     def call(self, view=None):
       
    65         if self.cw_rset is None:
       
    66             self.entity_call(self.cw_extra_kwargs.pop('entity'))
       
    67         else:
       
    68             self.cell_call(0, 0, view=view)
       
    69 
       
    70     def cell_call(self, row, col, view=None):
       
    71         self.entity_call(self.cw_rset.get_entity(row, col), view=view)
       
    72 
       
    73     def entity_call(self, entity, view=None):
       
    74         raise NotImplementedError()
       
    75 
       
    76 
    39 
    77 class NavigationComponent(Component):
    40 class NavigationComponent(Component):
    78     """abstract base class for navigation components"""
    41     """abstract base class for navigation components"""
    79     __regid__ = 'navigation'
    42     __regid__ = 'navigation'
    80     __select__ = paginated_rset()
    43     __select__ = paginated_rset()
   143         if view is not None and hasattr(view, 'page_navigation_url'):
   106         if view is not None and hasattr(view, 'page_navigation_url'):
   144             url = view.page_navigation_url(self, path, params)
   107             url = view.page_navigation_url(self, path, params)
   145         elif path == 'json':
   108         elif path == 'json':
   146             rql = params.pop('rql', self.cw_rset.printable_rql())
   109             rql = params.pop('rql', self.cw_rset.printable_rql())
   147             # latest 'true' used for 'swap' mode
   110             # latest 'true' used for 'swap' mode
   148             url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
   111             url = 'javascript: %s' % (js.replacePageChunk(
   149                 json_dumps(params.get('divid', 'pageContent')),
   112                 params.get('divid', 'pageContent'), rql,
   150                 json_dumps(rql), json_dumps(params.pop('vid', None)),
   113                 params.pop('vid', None), params))
   151                 json_dumps(params))
       
   152         else:
   114         else:
   153             url = self._cw.build_url(path, **params)
   115             url = self._cw.build_url(path, **params)
   154         return url
   116         return url
   155 
   117 
   156     def page_link(self, path, params, start, stop, content):
   118     def page_link(self, path, params, start, stop, content):
   173         if start >= self.total:
   135         if start >= self.total:
   174             return self.no_next_page_link
   136             return self.no_next_page_link
   175         stop = start + self.page_size - 1
   137         stop = start + self.page_size - 1
   176         url = xml_escape(self.page_url(path, params, start, stop))
   138         url = xml_escape(self.page_url(path, params, start, stop))
   177         return self.next_page_link_templ % (url, title, content)
   139         return self.next_page_link_templ % (url, title, content)
       
   140 
       
   141 
       
   142 # new contextual components system #############################################
       
   143 
       
   144 def override_ctx(cls, **kwargs):
       
   145     cwpdefs = cls.cw_property_defs.copy()
       
   146     cwpdefs['context']  = cwpdefs['context'].copy()
       
   147     cwpdefs['context'].update(kwargs)
       
   148     return cwpdefs
       
   149 
       
   150 
       
   151 class EmptyComponent(Exception):
       
   152     """some selectable component has actually no content and should not be
       
   153     rendered
       
   154     """
       
   155 
       
   156 class Layout(Component):
       
   157     __regid__ = 'layout'
       
   158     __abstract__ = True
       
   159 
       
   160     def init_rendering(self):
       
   161         """init view for rendering. Return true if we should go on, false
       
   162         if we should stop now.
       
   163         """
       
   164         view = self.cw_extra_kwargs['view']
       
   165         try:
       
   166             view.init_rendering()
       
   167         except Unauthorized, ex:
       
   168             self.warning("can't render %s: %s", view, ex)
       
   169             return False
       
   170         except EmptyComponent:
       
   171             return False
       
   172         return True
       
   173 
       
   174 
       
   175 class CtxComponent(AppObject):
       
   176     """base class for contextual compontents. The following contexts are
       
   177     predefined:
       
   178 
       
   179     * boxes: 'left', 'incontext', 'right'
       
   180     * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom'
       
   181     * other: 'ctxtoolbar'
       
   182 
       
   183     The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar'
       
   184     context are handled by the default primary view, others by the default main
       
   185     template.
       
   186 
       
   187     All subclasses may not support all those contexts (for instance if it can't
       
   188     be displayed as box, or as a toolbar icon). You may restrict allowed context
       
   189     as followed:
       
   190 
       
   191     .. sourcecode:: python
       
   192 
       
   193       class MyComponent(CtxComponent):
       
   194           cw_property_defs = override_ctx(CtxComponent,
       
   195                                           vocabulary=[list of contexts])
       
   196           context = 'my default context'
       
   197 
       
   198     You can configure default component's context by simply giving appropriate
       
   199     value to the `context` class attribute, as seen above.
       
   200     """
       
   201     __registry__ = 'ctxcomponents'
       
   202     __select__ = ~no_cnx()
       
   203 
       
   204     categories_in_order = ()
       
   205     cw_property_defs = {
       
   206         _('visible'): dict(type='Boolean', default=True,
       
   207                            help=_('display the box or not')),
       
   208         _('order'):   dict(type='Int', default=99,
       
   209                            help=_('display order of the box')),
       
   210         _('context'): dict(type='String', default='left',
       
   211                            vocabulary=(_('left'), _('incontext'), _('right'),
       
   212                                        _('navtop'), _('navbottom'),
       
   213                                        _('navcontenttop'), _('navcontentbottom'),
       
   214                                        _('ctxtoolbar')),
       
   215                            help=_('context where this component should be displayed')),
       
   216         }
       
   217     context = 'left'
       
   218     contextual = False
       
   219     title = None
       
   220 
       
   221     # XXX support kwargs for compat with old boxes which gets the view as
       
   222     # argument
       
   223     def render(self, w, **kwargs):
       
   224         getlayout = self._cw.vreg['components'].select
       
   225         try:
       
   226             # XXX ensure context is given when the component is reloaded through
       
   227             # ajax
       
   228             context = self.cw_extra_kwargs['context']
       
   229         except KeyError:
       
   230             context = self.cw_propval('context')
       
   231         layout = getlayout('layout', self._cw, rset=self.cw_rset,
       
   232                            row=self.cw_row, col=self.cw_col,
       
   233                            view=self, context=context)
       
   234         layout.render(w)
       
   235 
       
   236     def init_rendering(self):
       
   237         """init rendering callback: that's the good time to check your component
       
   238         has some content to display. If not, you can still raise
       
   239         :exc:`EmptyComponent` to inform it should be skipped.
       
   240 
       
   241         Also, :exc:`Unauthorized` will be catched, logged, then the component
       
   242         will be skipped.
       
   243         """
       
   244         self.items = []
       
   245 
       
   246     @property
       
   247     def domid(self):
       
   248         """return the HTML DOM identifier for this component"""
       
   249         return domid(self.__regid__)
       
   250 
       
   251     @property
       
   252     def cssclass(self):
       
   253         """return the CSS class name for this component"""
       
   254         return domid(self.__regid__)
       
   255 
       
   256     def render_title(self, w):
       
   257         """return the title for this component"""
       
   258         if self.title:
       
   259             w(self._cw._(self.title))
       
   260 
       
   261     def render_body(self, w):
       
   262         """return the body (content) for this component"""
       
   263         raise NotImplementedError()
       
   264 
       
   265     def render_items(self, w, items=None, klass=u'boxListing'):
       
   266         if items is None:
       
   267             items = self.items
       
   268         assert items
       
   269         w(u'<ul class="%s">' % klass)
       
   270         for item in items:
       
   271             if hasattr(item, 'render'):
       
   272                 item.render(w) # XXX display <li> by itself
       
   273             else:
       
   274                 w(u'<li>')
       
   275                 w(item)
       
   276                 w(u'</li>')
       
   277         w(u'</ul>')
       
   278 
       
   279     def append(self, item):
       
   280         self.items.append(item)
       
   281 
       
   282     def box_action(self, action): # XXX action_link
       
   283         return self.build_link(self._cw._(action.title), action.url())
       
   284 
       
   285     def build_link(self, title, url, **kwargs):
       
   286         if self._cw.selected(url):
       
   287             try:
       
   288                 kwargs['klass'] += ' selected'
       
   289             except KeyError:
       
   290                 kwargs['klass'] = 'selected'
       
   291         return tags.a(title, href=url, **kwargs)
       
   292 
       
   293 
       
   294 class EntityCtxComponent(CtxComponent):
       
   295     """base class for boxes related to a single entity"""
       
   296     __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset()
       
   297     context = 'incontext'
       
   298     contextual = True
       
   299 
       
   300     def __init__(self, *args, **kwargs):
       
   301         super(EntityCtxComponent, self).__init__(*args, **kwargs)
       
   302         try:
       
   303             entity = kwargs['entity']
       
   304         except KeyError:
       
   305             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   306         self.entity = entity
       
   307 
       
   308     @property
       
   309     def domid(self):
       
   310         return domid(self.__regid__) + unicode(self.entity.eid)
       
   311 
       
   312 
       
   313 # high level abstract classes ##################################################
       
   314 
       
   315 class RQLCtxComponent(CtxComponent):
       
   316     """abstract box for boxes displaying the content of a rql query not
       
   317     related to the current result set.
       
   318     """
       
   319     rql  = None
       
   320 
       
   321     def to_display_rql(self):
       
   322         assert self.rql is not None, self.__regid__
       
   323         return (self.rql,)
       
   324 
       
   325     def init_rendering(self):
       
   326         rset = self._cw.execute(*self.to_display_rql())
       
   327         if not rset:
       
   328             raise EmptyComponent()
       
   329         if len(rset[0]) == 2:
       
   330             self.items = []
       
   331             for i, (eid, label) in enumerate(rset):
       
   332                 entity = rset.get_entity(i, 0)
       
   333                 self.items.append(self.build_link(label, entity.absolute_url()))
       
   334         else:
       
   335             self.items = [self.build_link(e.dc_title(), e.absolute_url())
       
   336                           for e in rset.entities()]
       
   337 
       
   338     def render_body(self, w):
       
   339         self.render_items(w)
       
   340 
       
   341 
       
   342 class EditRelationMixIn(ReloadableMixIn):
       
   343     def box_item(self, entity, etarget, rql, label):
       
   344         """builds HTML link to edit relation between `entity` and `etarget`"""
       
   345         role, target = get_role(self), get_target(self)
       
   346         args = {role[0] : entity.eid, target[0] : etarget.eid}
       
   347         url = self._cw.user_rql_callback((rql, args))
       
   348         # for each target, provide a link to edit the relation
       
   349         return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
       
   350                                               etarget.view('incontext'))
       
   351 
       
   352     def related_boxitems(self, entity):
       
   353         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
   354         return [self.box_item(entity, etarget, rql, u'-')
       
   355                 for etarget in self.related_entities(entity)]
       
   356 
       
   357     def related_entities(self, entity):
       
   358         return entity.related(self.rtype, get_role(self), entities=True)
       
   359 
       
   360     def unrelated_boxitems(self, entity):
       
   361         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
   362         return [self.box_item(entity, etarget, rql, u'+')
       
   363                 for etarget in self.unrelated_entities(entity)]
       
   364 
       
   365     def unrelated_entities(self, entity):
       
   366         """returns the list of unrelated entities, using the entity's
       
   367         appropriate vocabulary function
       
   368         """
       
   369         skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
       
   370                                                           entities=True))
       
   371         skip.add(None)
       
   372         skip.add(INTERNAL_FIELD_VALUE)
       
   373         filteretype = getattr(self, 'etype', None)
       
   374         entities = []
       
   375         form = self._cw.vreg['forms'].select('edition', self._cw,
       
   376                                              rset=self.cw_rset,
       
   377                                              row=self.cw_row or 0)
       
   378         field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
       
   379         for _, eid in field.vocabulary(form):
       
   380             if eid not in skip:
       
   381                 entity = self._cw.entity_from_eid(eid)
       
   382                 if filteretype is None or entity.__regid__ == filteretype:
       
   383                     entities.append(entity)
       
   384         return entities
       
   385 
       
   386 
       
   387 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent):
       
   388     """base class for boxes which let add or remove entities linked by a given
       
   389     relation
       
   390 
       
   391     subclasses should define at least id, rtype and target class attributes.
       
   392     """
       
   393     def render_title(self, w):
       
   394         return display_name(self._cw, self.rtype, get_role(self),
       
   395                             context=self.entity.__regid__)
       
   396 
       
   397     def render_body(self, w):
       
   398         self._cw.add_js('cubicweb.ajax.js')
       
   399         related = self.related_boxitems(self.entity)
       
   400         unrelated = self.unrelated_boxitems(self.entity)
       
   401         self.items.extend(related)
       
   402         if related and unrelated:
       
   403             self.items.append(htmlwidgets.BoxSeparator())
       
   404         self.items.extend(unrelated)
       
   405         self.render_items(w)
       
   406 
       
   407 
       
   408 class AjaxEditRelationCtxComponent(EntityCtxComponent):
       
   409     __select__ = EntityCtxComponent.__select__ & (
       
   410         partial_relation_possible(action='add') | partial_has_related_entities())
       
   411 
       
   412     # view used to display related entties
       
   413     item_vid = 'incontext'
       
   414     # values separator when multiple values are allowed
       
   415     separator = ','
       
   416     # msgid of the message to display when some new relation has been added/removed
       
   417     added_msg = None
       
   418     removed_msg = None
       
   419 
       
   420     # class attributes below *must* be set in concret classes (additionaly to
       
   421     # rtype / role [/ target_etype]. They should correspond to js_* methods on
       
   422     # the json controller
       
   423 
       
   424     # function(eid)
       
   425     # -> expected to return a list of values to display as input selector
       
   426     #    vocabulary
       
   427     fname_vocabulary = None
       
   428 
       
   429     # function(eid, value)
       
   430     # -> handle the selector's input (eg create necessary entities and/or
       
   431     # relations). If the relation is multiple, you'll get a list of value, else
       
   432     # a single string value.
       
   433     fname_validate = None
       
   434 
       
   435     # function(eid, linked entity eid)
       
   436     # -> remove the relation
       
   437     fname_remove = None
       
   438 
       
   439     def __init__(self, *args, **kwargs):
       
   440         super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs)
       
   441         self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
       
   442 
       
   443     def render_title(self, w):
       
   444         w(self.rdef.rtype.display_name(self._cw, self.role,
       
   445                                        context=self.entity.__regid__))
       
   446 
       
   447     def render_body(self, w):
       
   448         req = self._cw
       
   449         entity = self.entity
       
   450         related = entity.related(self.rtype, self.role)
       
   451         if self.role == 'subject':
       
   452             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
       
   453             maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
       
   454         else:
       
   455             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
       
   456             maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
       
   457         if mayadd or maydel:
       
   458             req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
       
   459         _ = req._
       
   460         if related:
       
   461             w(u'<table>')
       
   462             for rentity in related.entities():
       
   463                 # for each related entity, provide a link to remove the relation
       
   464                 subview = rentity.view(self.item_vid)
       
   465                 if maydel:
       
   466                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
       
   467                         self.__regid__, entity.eid, rentity.eid,
       
   468                         self.fname_remove,
       
   469                         self.removed_msg and _(self.removed_msg)))
       
   470                     w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
       
   471                       '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall),
       
   472                                                             subview))
       
   473                 else:
       
   474                     w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
       
   475             w(u'</table>')
       
   476         else:
       
   477             w(_('no related entity'))
       
   478         if mayadd:
       
   479             req.add_js('jquery.autocomplete.js')
       
   480             req.add_css('jquery.autocomplete.css')
       
   481             multiple = self.rdef.role_cardinality(self.role) in '*+'
       
   482             w(u'<table><tr><td>')
       
   483             jscall = unicode(js.ajaxBoxShowSelector(
       
   484                 self.__regid__, entity.eid, self.fname_vocabulary,
       
   485                 self.fname_validate, self.added_msg and _(self.added_msg),
       
   486                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
       
   487                 multiple and self.separator))
       
   488             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
       
   489                 xml_escape(jscall),
       
   490                 multiple and _('add_relation') or _('update_relation')))
       
   491             w(u'</td><td>')
       
   492             w(u'<div id="%sHolder"></div>' % self.domid)
       
   493             w(u'</td></tr></table>')
       
   494 
       
   495 
       
   496 # old contextual components, deprecated ########################################
       
   497 
       
   498 class EntityVComponent(Component):
       
   499     """abstract base class for additinal components displayed in content
       
   500     headers and footer according to:
       
   501 
       
   502     * the displayed entity's type
       
   503     * a context (currently 'header' or 'footer')
       
   504 
       
   505     it should be configured using .accepts, .etype, .rtype, .target and
       
   506     .context class attributes
       
   507     """
       
   508     __metaclass__ = class_deprecated
       
   509     __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'
       
   510 
       
   511     __registry__ = 'ctxcomponents'
       
   512     __select__ = one_line_rset()
       
   513 
       
   514     cw_property_defs = {
       
   515         _('visible'):  dict(type='Boolean', default=True,
       
   516                             help=_('display the component or not')),
       
   517         _('order'):    dict(type='Int', default=99,
       
   518                             help=_('display order of the component')),
       
   519         _('context'):  dict(type='String', default='navtop',
       
   520                             vocabulary=(_('navtop'), _('navbottom'),
       
   521                                         _('navcontenttop'), _('navcontentbottom'),
       
   522                                         _('ctxtoolbar')),
       
   523                             help=_('context where this component should be displayed')),
       
   524     }
       
   525 
       
   526     context = 'navcontentbottom'
       
   527 
       
   528     def call(self, view=None):
       
   529         if self.cw_rset is None:
       
   530             self.entity_call(self.cw_extra_kwargs.pop('entity'))
       
   531         else:
       
   532             self.cell_call(0, 0, view=view)
       
   533 
       
   534     def cell_call(self, row, col, view=None):
       
   535         self.entity_call(self.cw_rset.get_entity(row, col), view=view)
       
   536 
       
   537     def entity_call(self, entity, view=None):
       
   538         raise NotImplementedError()
   178 
   539 
   179 
   540 
   180 class RelatedObjectsVComponent(EntityVComponent):
   541 class RelatedObjectsVComponent(EntityVComponent):
   181     """a section to display some related entities"""
   542     """a section to display some related entities"""
   182     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
   543     __select__ = EntityVComponent.__select__ & partial_has_related_entities()
   201         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
   562         self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
   202         self.wview(self.vid, rset)
   563         self.wview(self.vid, rset)
   203         self.w(u'</div>')
   564         self.w(u'</div>')
   204 
   565 
   205 
   566 
       
   567 
   206 VComponent = class_renamed('VComponent', Component,
   568 VComponent = class_renamed('VComponent', Component,
   207                            'VComponent is deprecated, use Component')
   569                            '[3.2] VComponent is deprecated, use Component')
   208 SingletonVComponent = class_renamed('SingletonVComponent', Component,
   570 SingletonVComponent = class_renamed('SingletonVComponent', Component,
   209                                     'SingletonVComponent is deprecated, use '
   571                                     '[3.2] SingletonVComponent is deprecated, use '
   210                                     'Component and explicit registration control')
   572                                     'Component and explicit registration control')