web/box.py
changeset 6141 b8287e54b528
parent 6140 65a619eb31c4
child 6490 34359fbde6ef
equal deleted inserted replaced
6140:65a619eb31c4 6141:b8287e54b528
    21 _ = unicode
    21 _ = unicode
    22 
    22 
    23 from logilab.mtconverter import xml_escape
    23 from logilab.mtconverter import xml_escape
    24 from logilab.common.deprecation import class_deprecated, class_renamed
    24 from logilab.common.deprecation import class_deprecated, class_renamed
    25 
    25 
    26 from cubicweb import Unauthorized, role as get_role, target as get_target, tags
    26 from cubicweb import Unauthorized, role as get_role, target as get_target
    27 from cubicweb.schema import display_name
    27 from cubicweb.schema import display_name
    28 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
    28 from cubicweb.selectors import no_cnx, one_line_rset
    29                                 match_context_prop, partial_relation_possible,
    29 from cubicweb.view import View
    30                                 partial_has_related_entities)
       
    31 from cubicweb.appobject import AppObject
       
    32 from cubicweb.view import View, ReloadableMixIn, Component
       
    33 from cubicweb.uilib import domid, js
       
    34 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
    30 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
    35 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    31 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    36                                       RawBoxItem, BoxSeparator)
    32                                       RawBoxItem, BoxSeparator)
    37 from cubicweb.web.action import UnregisteredAction
    33 from cubicweb.web.action import UnregisteredAction
    38 
    34 
    53     for item in sorted(actions_by_cat.items()):
    49     for item in sorted(actions_by_cat.items()):
    54         result.append(item)
    50         result.append(item)
    55     return result
    51     return result
    56 
    52 
    57 
    53 
    58 class EditRelationMixIn(ReloadableMixIn):
       
    59     def box_item(self, entity, etarget, rql, label):
       
    60         """builds HTML link to edit relation between `entity` and `etarget`"""
       
    61         role, target = get_role(self), get_target(self)
       
    62         args = {role[0] : entity.eid, target[0] : etarget.eid}
       
    63         url = self._cw.user_rql_callback((rql, args))
       
    64         # for each target, provide a link to edit the relation
       
    65         return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
       
    66                                               etarget.view('incontext'))
       
    67 
       
    68     def related_boxitems(self, entity):
       
    69         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
    70         return [self.box_item(entity, etarget, rql, u'-')
       
    71                 for etarget in self.related_entities(entity)]
       
    72 
       
    73     def related_entities(self, entity):
       
    74         return entity.related(self.rtype, get_role(self), entities=True)
       
    75 
       
    76     def unrelated_boxitems(self, entity):
       
    77         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
    78         return [self.box_item(entity, etarget, rql, u'+')
       
    79                 for etarget in self.unrelated_entities(entity)]
       
    80 
       
    81     def unrelated_entities(self, entity):
       
    82         """returns the list of unrelated entities, using the entity's
       
    83         appropriate vocabulary function
       
    84         """
       
    85         skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self),
       
    86                                                           entities=True))
       
    87         skip.add(None)
       
    88         skip.add(INTERNAL_FIELD_VALUE)
       
    89         filteretype = getattr(self, 'etype', None)
       
    90         entities = []
       
    91         form = self._cw.vreg['forms'].select('edition', self._cw,
       
    92                                              rset=self.cw_rset,
       
    93                                              row=self.cw_row or 0)
       
    94         field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
       
    95         for _, eid in field.vocabulary(form):
       
    96             if eid not in skip:
       
    97                 entity = self._cw.entity_from_eid(eid)
       
    98                 if filteretype is None or entity.__regid__ == filteretype:
       
    99                     entities.append(entity)
       
   100         return entities
       
   101 
       
   102 
       
   103 # generic classes for the new box system #######################################
       
   104 
       
   105 from cubicweb.selectors import match_context, contextual
       
   106 
       
   107 class EmptyComponent(Exception):
       
   108     """some selectable component has actually no content and should not be
       
   109     rendered
       
   110     """
       
   111 
       
   112 class Layout(Component):
       
   113     __regid__ = 'layout'
       
   114     __abstract__ = True
       
   115 
       
   116 
       
   117 class Box(AppObject): # XXX ContextComponent
       
   118     __registry__ = 'boxes'
       
   119     __select__ = ~no_cnx() & match_context_prop()
       
   120 
       
   121     categories_in_order = ()
       
   122     cw_property_defs = {
       
   123         _('visible'): dict(type='Boolean', default=True,
       
   124                            help=_('display the box or not')),
       
   125         _('order'):   dict(type='Int', default=99,
       
   126                            help=_('display order of the box')),
       
   127         # XXX 'incontext' boxes are handled by the default primary view
       
   128         _('context'): dict(type='String', default='left',
       
   129                            vocabulary=(_('left'), _('incontext'), _('right')),
       
   130                            help=_('context where this box should be displayed')),
       
   131         }
       
   132     context = 'left'
       
   133     contextual = False
       
   134     title = None
       
   135     # XXX support kwargs for compat with old boxes which gets the view as
       
   136     # argument
       
   137     def render(self, w, **kwargs):
       
   138         getlayout = self._cw.vreg['components'].select
       
   139         try:
       
   140             # XXX ensure context is given when the component is reloaded through
       
   141             # ajax
       
   142             context = self.cw_extra_kwargs['context']
       
   143         except KeyError:
       
   144             context = self.cw_propval('context')
       
   145         layout = getlayout('layout', self._cw, rset=self.cw_rset,
       
   146                            row=self.cw_row, col=self.cw_col,
       
   147                            view=self, context=context)
       
   148         layout.render(w)
       
   149 
       
   150     def init_rendering(self):
       
   151         """init rendering callback: that's the good time to check your component
       
   152         has some content to display. If not, you can still raise
       
   153         :exc:`EmptyComponent` to inform it should be skipped.
       
   154 
       
   155         Also, :exc:`Unauthorized` will be catched, logged, then the component
       
   156         will be skipped.
       
   157         """
       
   158         self.items = []
       
   159 
       
   160     @property
       
   161     def domid(self):
       
   162         """return the HTML DOM identifier for this component"""
       
   163         return domid(self.__regid__)
       
   164 
       
   165     @property
       
   166     def cssclass(self):
       
   167         """return the CSS class name for this component"""
       
   168         return domid(self.__regid__)
       
   169 
       
   170     def render_title(self, w):
       
   171         """return the title for this component"""
       
   172         if self.title is None:
       
   173             raise NotImplementedError()
       
   174         w(self._cw._(self.title))
       
   175 
       
   176     def render_body(self, w):
       
   177         """return the body (content) for this component"""
       
   178         raise NotImplementedError()
       
   179 
       
   180     def render_items(self, w, items=None, klass=u'boxListing'):
       
   181         if items is None:
       
   182             items = self.items
       
   183         assert items
       
   184         w(u'<ul class="%s">' % klass)
       
   185         for item in items:
       
   186             if hasattr(item, 'render'):
       
   187                 item.render(w) # XXX display <li> by itself
       
   188             else:
       
   189                 w(u'<li>')
       
   190                 w(item)
       
   191                 w(u'</li>')
       
   192         w(u'</ul>')
       
   193 
       
   194     def append(self, item):
       
   195         self.items.append(item)
       
   196 
       
   197     def box_action(self, action): # XXX action_link
       
   198         return self.build_link(self._cw._(action.title), action.url())
       
   199 
       
   200     def build_link(self, title, url, **kwargs):
       
   201         if self._cw.selected(url):
       
   202             try:
       
   203                 kwargs['klass'] += ' selected'
       
   204             except KeyError:
       
   205                 kwargs['klass'] = 'selected'
       
   206         return tags.a(title, href=url, **kwargs)
       
   207 
       
   208 
       
   209 class EntityBox(Box): # XXX ContextEntityComponent
       
   210     """base class for boxes related to a single entity"""
       
   211     __select__ = Box.__select__ & one_line_rset()
       
   212     context = 'incontext'
       
   213     contextual = True
       
   214 
       
   215     def __init__(self, *args, **kwargs):
       
   216         super(EntityBox, self).__init__(*args, **kwargs)
       
   217         try:
       
   218             entity = kwargs['entity']
       
   219         except KeyError:
       
   220             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   221         self.entity = entity
       
   222 
       
   223     @property
       
   224     def domid(self):
       
   225         return domid(self.__regid__) + unicode(self.entity.eid)
       
   226 
       
   227 
       
   228 # high level abstract box classes ##############################################
       
   229 
       
   230 
       
   231 class RQLBox(Box):
       
   232     """abstract box for boxes displaying the content of a rql query not
       
   233     related to the current result set.
       
   234     """
       
   235     rql  = None
       
   236 
       
   237     def to_display_rql(self):
       
   238         assert self.rql is not None, self.__regid__
       
   239         return (self.rql,)
       
   240 
       
   241     def init_rendering(self):
       
   242         rset = self._cw.execute(*self.to_display_rql())
       
   243         if not rset:
       
   244             raise EmptyComponent()
       
   245         if len(rset[0]) == 2:
       
   246             self.items = []
       
   247             for i, (eid, label) in enumerate(rset):
       
   248                 entity = rset.get_entity(i, 0)
       
   249                 self.items.append(self.build_link(label, entity.absolute_url()))
       
   250         else:
       
   251             self.items = [self.build_link(e.dc_title(), e.absolute_url())
       
   252                           for e in rset.entities()]
       
   253 
       
   254     def render_body(self, w):
       
   255         self.render_items(w)
       
   256 
       
   257 
       
   258 class EditRelationBox(EditRelationMixIn, EntityBox):
       
   259     """base class for boxes which let add or remove entities linked by a given
       
   260     relation
       
   261 
       
   262     subclasses should define at least id, rtype and target class attributes.
       
   263     """
       
   264     def render_title(self, w):
       
   265         return display_name(self._cw, self.rtype, get_role(self),
       
   266                             context=self.entity.__regid__)
       
   267 
       
   268     def render_body(self, w):
       
   269         self._cw.add_js('cubicweb.ajax.js')
       
   270         related = self.related_boxitems(self.entity)
       
   271         unrelated = self.unrelated_boxitems(self.entity)
       
   272         self.items.extend(related)
       
   273         if related and unrelated:
       
   274             self.items.append(BoxSeparator())
       
   275         self.items.extend(unrelated)
       
   276         self.render_items(w)
       
   277 
       
   278 
       
   279 class AjaxEditRelationBox(EntityBox):
       
   280     __select__ = EntityBox.__select__ & (
       
   281         partial_relation_possible(action='add') | partial_has_related_entities())
       
   282 
       
   283     # view used to display related entties
       
   284     item_vid = 'incontext'
       
   285     # values separator when multiple values are allowed
       
   286     separator = ','
       
   287     # msgid of the message to display when some new relation has been added/removed
       
   288     added_msg = None
       
   289     removed_msg = None
       
   290 
       
   291     # class attributes below *must* be set in concret classes (additionaly to
       
   292     # rtype / role [/ target_etype]. They should correspond to js_* methods on
       
   293     # the json controller
       
   294 
       
   295     # function(eid)
       
   296     # -> expected to return a list of values to display as input selector
       
   297     #    vocabulary
       
   298     fname_vocabulary = None
       
   299 
       
   300     # function(eid, value)
       
   301     # -> handle the selector's input (eg create necessary entities and/or
       
   302     # relations). If the relation is multiple, you'll get a list of value, else
       
   303     # a single string value.
       
   304     fname_validate = None
       
   305 
       
   306     # function(eid, linked entity eid)
       
   307     # -> remove the relation
       
   308     fname_remove = None
       
   309 
       
   310     def __init__(self, *args, **kwargs):
       
   311         super(AjaxEditRelationBox, self).__init__(*args, **kwargs)
       
   312         self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
       
   313 
       
   314     def render_title(self, w):
       
   315         w(self.rdef.rtype.display_name(self._cw, self.role,
       
   316                                        context=self.entity.__regid__))
       
   317 
       
   318     def render_body(self, w):
       
   319         req = self._cw
       
   320         entity = self.entity
       
   321         related = entity.related(self.rtype, self.role)
       
   322         if self.role == 'subject':
       
   323             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
       
   324             maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
       
   325         else:
       
   326             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
       
   327             maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
       
   328         if mayadd or maydel:
       
   329             req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
       
   330         _ = req._
       
   331         if related:
       
   332             w(u'<table>')
       
   333             for rentity in related.entities():
       
   334                 # for each related entity, provide a link to remove the relation
       
   335                 subview = rentity.view(self.item_vid)
       
   336                 if maydel:
       
   337                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
       
   338                         self.__regid__, entity.eid, rentity.eid,
       
   339                         self.fname_remove,
       
   340                         self.removed_msg and _(self.removed_msg)))
       
   341                     w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
       
   342                       '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall),
       
   343                                                             subview))
       
   344                 else:
       
   345                     w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
       
   346             w(u'</table>')
       
   347         else:
       
   348             w(_('no related entity'))
       
   349         if mayadd:
       
   350             req.add_js('jquery.autocomplete.js')
       
   351             req.add_css('jquery.autocomplete.css')
       
   352             multiple = self.rdef.role_cardinality(self.role) in '*+'
       
   353             w(u'<table><tr><td>')
       
   354             jscall = unicode(js.ajaxBoxShowSelector(
       
   355                 self.__regid__, entity.eid, self.fname_vocabulary,
       
   356                 self.fname_validate, self.added_msg and _(self.added_msg),
       
   357                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
       
   358                 multiple and self.separator))
       
   359             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
       
   360                 xml_escape(jscall),
       
   361                 multiple and _('add_relation') or _('update_relation')))
       
   362             w(u'</td><td>')
       
   363             w(u'<div id="%sHolder"></div>' % self.domid)
       
   364             w(u'</td></tr></table>')
       
   365 
       
   366 
       
   367 # old box system, deprecated ###################################################
    54 # old box system, deprecated ###################################################
   368 
    55 
   369 class BoxTemplate(View):
    56 class BoxTemplate(View):
   370     """base template for boxes, usually a (contextual) list of possible
    57     """base template for boxes, usually a (contextual) list of possible
   371 
       
   372     actions. Various classes attributes may be used to control the box
    58     actions. Various classes attributes may be used to control the box
   373     rendering.
    59     rendering.
   374 
    60 
   375     You may override on of the formatting callbacks is this is not necessary
    61     You may override one of the formatting callbacks if this is not necessary
   376     for your custom box.
    62     for your custom box.
   377 
    63 
   378     Classes inheriting from this class usually only have to override call
    64     Classes inheriting from this class usually only have to override call
   379     to fetch desired actions, and then to do something like  ::
    65     to fetch desired actions, and then to do something like  ::
   380 
    66 
   381         box.render(self.w)
    67         box.render(self.w)
   382     """
    68     """
   383     __metaclass__ = class_deprecated
    69     __metaclass__ = class_deprecated
   384     __deprecation_warning__ = '*BoxTemplate classes are deprecated, use *Box instead'
    70     __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)'
   385 
    71 
   386     __registry__ = 'boxes'
    72     __registry__ = 'ctxcomponents'
   387     __select__ = ~no_cnx() & match_context_prop()
    73     __select__ = ~no_cnx()
   388 
    74 
   389     categories_in_order = ()
    75     categories_in_order = ()
   390     cw_property_defs = {
    76     cw_property_defs = {
   391         _('visible'): dict(type='Boolean', default=True,
    77         _('visible'): dict(type='Boolean', default=True,
   392                            help=_('display the box or not')),
    78                            help=_('display the box or not')),
   463         return (self.rql, {'x': self._cw.user.eid})
   149         return (self.rql, {'x': self._cw.user.eid})
   464 
   150 
   465 
   151 
   466 class EntityBoxTemplate(BoxTemplate):
   152 class EntityBoxTemplate(BoxTemplate):
   467     """base class for boxes related to a single entity"""
   153     """base class for boxes related to a single entity"""
   468     __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
   154     __select__ = BoxTemplate.__select__ & one_line_rset()
   469     context = 'incontext'
   155     context = 'incontext'
   470 
   156 
   471     def call(self, row=0, col=0, **kwargs):
   157     def call(self, row=0, col=0, **kwargs):
   472         """classes inheriting from EntityBoxTemplate should define cell_call"""
   158         """classes inheriting from EntityBoxTemplate should define cell_call"""
   473         self.cell_call(row, col, **kwargs)
   159         self.cell_call(row, col, **kwargs)
       
   160 
       
   161 from cubicweb.web.component import AjaxEditRelationCtxComponent, EditRelationMixIn
   474 
   162 
   475 
   163 
   476 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
   164 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
   477     """base class for boxes which let add or remove entities linked
   165     """base class for boxes which let add or remove entities linked
   478     by a given relation
   166     by a given relation
   500             entity, etarget, rql, label)
   188             entity, etarget, rql, label)
   501         return RawBoxItem(label, liclass=u'invisible')
   189         return RawBoxItem(label, liclass=u'invisible')
   502 
   190 
   503 
   191 
   504 AjaxEditRelationBoxTemplate = class_renamed(
   192 AjaxEditRelationBoxTemplate = class_renamed(
   505     'AjaxEditRelationBoxTemplate', AjaxEditRelationBox,
   193     'AjaxEditRelationBoxTemplate', AjaxEditRelationCtxComponent,
   506     '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationBox')
   194     '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationCtxComponent')
   507 
   195