web/box.py
changeset 6140 65a619eb31c4
parent 6017 5f6a60ea8544
child 6141 b8287e54b528
equal deleted inserted replaced
6139:f76599a96238 6140:65a619eb31c4
    19 
    19 
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    21 _ = unicode
    21 _ = unicode
    22 
    22 
    23 from logilab.mtconverter import xml_escape
    23 from logilab.mtconverter import xml_escape
    24 
    24 from logilab.common.deprecation import class_deprecated, class_renamed
    25 from cubicweb import Unauthorized, role as get_role, target as get_target
    25 
       
    26 from cubicweb import Unauthorized, role as get_role, target as get_target, tags
    26 from cubicweb.schema import display_name
    27 from cubicweb.schema import display_name
    27 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
    28 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
    28                                 match_context_prop, partial_relation_possible,
    29                                 match_context_prop, partial_relation_possible,
    29                                 partial_has_related_entities)
    30                                 partial_has_related_entities)
    30 from cubicweb.view import View, ReloadableMixIn
    31 from cubicweb.appobject import AppObject
       
    32 from cubicweb.view import View, ReloadableMixIn, Component
    31 from cubicweb.uilib import domid, js
    33 from cubicweb.uilib import domid, js
    32 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
    34 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
    33 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    35 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    34                                       RawBoxItem, BoxSeparator)
    36                                       RawBoxItem, BoxSeparator)
    35 from cubicweb.web.action import UnregisteredAction
    37 from cubicweb.web.action import UnregisteredAction
    36 
    38 
    37 
    39 
    38 class BoxTemplate(View):
    40 def sort_by_category(actions, categories_in_order=None):
    39     """base template for boxes, usually a (contextual) list of possible
    41     """return a list of (category, actions_sorted_by_title)"""
    40 
    42     result = []
    41     actions. Various classes attributes may be used to control the box
    43     actions_by_cat = {}
    42     rendering.
    44     for action in actions:
    43 
    45         actions_by_cat.setdefault(action.category, []).append(
    44     You may override on of the formatting callbacks is this is not necessary
    46             (action.title, action) )
    45     for your custom box.
    47     for key, values in actions_by_cat.items():
    46 
    48         actions_by_cat[key] = [act for title, act in sorted(values)]
    47     Classes inheriting from this class usually only have to override call
    49     if categories_in_order:
    48     to fetch desired actions, and then to do something like  ::
    50         for cat in categories_in_order:
    49 
       
    50         box.render(self.w)
       
    51     """
       
    52     __registry__ = 'boxes'
       
    53     __select__ = ~no_cnx() & match_context_prop()
       
    54 
       
    55     categories_in_order = ()
       
    56     cw_property_defs = {
       
    57         _('visible'): dict(type='Boolean', default=True,
       
    58                            help=_('display the box or not')),
       
    59         _('order'):   dict(type='Int', default=99,
       
    60                            help=_('display order of the box')),
       
    61         # XXX 'incontext' boxes are handled by the default primary view
       
    62         _('context'): dict(type='String', default='left',
       
    63                            vocabulary=(_('left'), _('incontext'), _('right')),
       
    64                            help=_('context where this box should be displayed')),
       
    65         }
       
    66     context = 'left'
       
    67     htmlitemclass = 'boxItem'
       
    68 
       
    69     def sort_actions(self, actions):
       
    70         """return a list of (category, actions_sorted_by_title)"""
       
    71         result = []
       
    72         actions_by_cat = {}
       
    73         for action in actions:
       
    74             actions_by_cat.setdefault(action.category, []).append(
       
    75                 (action.title, action) )
       
    76         for key, values in actions_by_cat.items():
       
    77             actions_by_cat[key] = [act for title, act in sorted(values)]
       
    78         for cat in self.categories_in_order:
       
    79             if cat in actions_by_cat:
    51             if cat in actions_by_cat:
    80                 result.append( (cat, actions_by_cat[cat]) )
    52                 result.append( (cat, actions_by_cat[cat]) )
    81         for item in sorted(actions_by_cat.items()):
    53     for item in sorted(actions_by_cat.items()):
    82             result.append(item)
    54         result.append(item)
    83         return result
    55     return result
    84 
    56 
    85     def mk_action(self, title, path, escape=True, **kwargs):
    57 
    86         """factory function to create dummy actions compatible with the
    58 class EditRelationMixIn(ReloadableMixIn):
    87         .format_actions method
       
    88         """
       
    89         if escape:
       
    90             title = xml_escape(title)
       
    91         return self.box_action(self._action(title, path, **kwargs))
       
    92 
       
    93     def _action(self, title, path, **kwargs):
       
    94         return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs)
       
    95 
       
    96     # formating callbacks
       
    97 
       
    98     def boxitem_link_tooltip(self, action):
       
    99         if action.__regid__:
       
   100             return u'keyword: %s' % action.__regid__
       
   101         return u''
       
   102 
       
   103     def box_action(self, action):
       
   104         cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
       
   105         return BoxLink(action.url(), self._cw._(action.title),
       
   106                        cls, self.boxitem_link_tooltip(action))
       
   107 
       
   108 
       
   109 class RQLBoxTemplate(BoxTemplate):
       
   110     """abstract box for boxes displaying the content of a rql query not
       
   111     related to the current result set.
       
   112 
       
   113     It rely on etype, rtype (both optional, usable to control registration
       
   114     according to application schema and display according to connected
       
   115     user's rights) and rql attributes
       
   116     """
       
   117 
       
   118     rql  = None
       
   119 
       
   120     def to_display_rql(self):
       
   121         assert self.rql is not None, self.__regid__
       
   122         return (self.rql,)
       
   123 
       
   124     def call(self, **kwargs):
       
   125         try:
       
   126             rset = self._cw.execute(*self.to_display_rql())
       
   127         except Unauthorized:
       
   128             # can't access to something in the query, forget this box
       
   129             return
       
   130         if len(rset) == 0:
       
   131             return
       
   132         box = BoxWidget(self._cw._(self.title), self.__regid__)
       
   133         for i, (teid, tname) in enumerate(rset):
       
   134             entity = rset.get_entity(i, 0)
       
   135             box.append(self.mk_action(tname, entity.absolute_url()))
       
   136         box.render(w=self.w)
       
   137 
       
   138 
       
   139 class UserRQLBoxTemplate(RQLBoxTemplate):
       
   140     """same as rql box template but the rql is build using the eid of the
       
   141     request's user
       
   142     """
       
   143 
       
   144     def to_display_rql(self):
       
   145         assert self.rql is not None, self.__regid__
       
   146         return (self.rql, {'x': self._cw.user.eid})
       
   147 
       
   148 
       
   149 class EntityBoxTemplate(BoxTemplate):
       
   150     """base class for boxes related to a single entity"""
       
   151     __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
       
   152     context = 'incontext'
       
   153 
       
   154     def call(self, row=0, col=0, **kwargs):
       
   155         """classes inheriting from EntityBoxTemplate should define cell_call"""
       
   156         self.cell_call(row, col, **kwargs)
       
   157 
       
   158 
       
   159 class RelatedEntityBoxTemplate(EntityBoxTemplate):
       
   160     __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
       
   161 
       
   162     def cell_call(self, row, col, **kwargs):
       
   163         entity = self.cw_rset.get_entity(row, col)
       
   164         limit = self._cw.property_value('navigation.related-limit') + 1
       
   165         role = get_role(self)
       
   166         self.w(u'<div class="sideBox">')
       
   167         self.wview('sidebox', entity.related(self.rtype, role, limit=limit),
       
   168                    title=display_name(self._cw, self.rtype, role,
       
   169                                       context=entity.__regid__))
       
   170         self.w(u'</div>')
       
   171 
       
   172 
       
   173 class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate):
       
   174     """base class for boxes which let add or remove entities linked
       
   175     by a given relation
       
   176 
       
   177     subclasses should define at least id, rtype and target
       
   178     class attributes.
       
   179     """
       
   180 
       
   181     def cell_call(self, row, col, view=None, **kwargs):
       
   182         self._cw.add_js('cubicweb.ajax.js')
       
   183         entity = self.cw_rset.get_entity(row, col)
       
   184         title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__)
       
   185         box = SideBoxWidget(title, self.__regid__)
       
   186         related = self.related_boxitems(entity)
       
   187         unrelated = self.unrelated_boxitems(entity)
       
   188         box.extend(related)
       
   189         if related and unrelated:
       
   190             box.append(BoxSeparator())
       
   191         box.extend(unrelated)
       
   192         box.render(self.w)
       
   193 
       
   194     def div_id(self):
       
   195         return self.__regid__
       
   196 
       
   197     def box_item(self, entity, etarget, rql, label):
    59     def box_item(self, entity, etarget, rql, label):
   198         """builds HTML link to edit relation between `entity` and `etarget`
    60         """builds HTML link to edit relation between `entity` and `etarget`"""
   199         """
       
   200         role, target = get_role(self), get_target(self)
    61         role, target = get_role(self), get_target(self)
   201         args = {role[0] : entity.eid, target[0] : etarget.eid}
    62         args = {role[0] : entity.eid, target[0] : etarget.eid}
   202         url = self._cw.user_rql_callback((rql, args))
    63         url = self._cw.user_rql_callback((rql, args))
   203         # for each target, provide a link to edit the relation
    64         # for each target, provide a link to edit the relation
   204         label = u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
    65         return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label,
   205                                                etarget.view('incontext'))
    66                                               etarget.view('incontext'))
   206         return RawBoxItem(label, liclass=u'invisible')
       
   207 
    67 
   208     def related_boxitems(self, entity):
    68     def related_boxitems(self, entity):
   209         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
    69         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
   210         related = []
    70         return [self.box_item(entity, etarget, rql, u'-')
   211         for etarget in self.related_entities(entity):
    71                 for etarget in self.related_entities(entity)]
   212             related.append(self.box_item(entity, etarget, rql, u'-'))
    72 
   213         return related
    73     def related_entities(self, entity):
       
    74         return entity.related(self.rtype, get_role(self), entities=True)
   214 
    75 
   215     def unrelated_boxitems(self, entity):
    76     def unrelated_boxitems(self, entity):
   216         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
    77         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
   217         unrelated = []
    78         return [self.box_item(entity, etarget, rql, u'+')
   218         for etarget in self.unrelated_entities(entity):
    79                 for etarget in self.unrelated_entities(entity)]
   219             unrelated.append(self.box_item(entity, etarget, rql, u'+'))
       
   220         return unrelated
       
   221 
       
   222     def related_entities(self, entity):
       
   223         return entity.related(self.rtype, get_role(self), entities=True)
       
   224 
    80 
   225     def unrelated_entities(self, entity):
    81     def unrelated_entities(self, entity):
   226         """returns the list of unrelated entities, using the entity's
    82         """returns the list of unrelated entities, using the entity's
   227         appropriate vocabulary function
    83         appropriate vocabulary function
   228         """
    84         """
   242                 if filteretype is None or entity.__regid__ == filteretype:
    98                 if filteretype is None or entity.__regid__ == filteretype:
   243                     entities.append(entity)
    99                     entities.append(entity)
   244         return entities
   100         return entities
   245 
   101 
   246 
   102 
   247 class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
   103 # generic classes for the new box system #######################################
   248     __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
   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())
   249 
   282 
   250     # view used to display related entties
   283     # view used to display related entties
   251     item_vid = 'incontext'
   284     item_vid = 'incontext'
   252     # values separator when multiple values are allowed
   285     # values separator when multiple values are allowed
   253     separator = ','
   286     separator = ','
   272 
   305 
   273     # function(eid, linked entity eid)
   306     # function(eid, linked entity eid)
   274     # -> remove the relation
   307     # -> remove the relation
   275     fname_remove = None
   308     fname_remove = None
   276 
   309 
   277     def cell_call(self, row, col, **kwargs):
   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):
   278         req = self._cw
   319         req = self._cw
   279         entity = self.cw_rset.get_entity(row, col)
   320         entity = self.entity
   280         related = entity.related(self.rtype, self.role)
   321         related = entity.related(self.rtype, self.role)
   281         rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
       
   282         if self.role == 'subject':
   322         if self.role == 'subject':
   283             mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
   323             mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
   284             maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
   324             maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid)
   285         else:
   325         else:
   286             mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
   326             mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
   287             maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
   327             maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid)
   288         if not (related or mayadd):
       
   289             return
       
   290         if mayadd or maydel:
   328         if mayadd or maydel:
   291             req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
   329             req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
   292         _ = req._
   330         _ = req._
   293         w = self.w
       
   294         divid = domid(self.__regid__) + unicode(entity.eid)
       
   295         w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
       
   296         w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
       
   297                rdef.rtype.display_name(req, self.role, context=entity.__regid__))
       
   298         w(u'<div class="sideBox"><div class="sideBoxBody">')
       
   299         if related:
   331         if related:
   300             w(u'<table>')
   332             w(u'<table>')
   301             for rentity in related.entities():
   333             for rentity in related.entities():
   302                 # for each related entity, provide a link to remove the relation
   334                 # for each related entity, provide a link to remove the relation
   303                 subview = rentity.view(self.item_vid)
   335                 subview = rentity.view(self.item_vid)
   305                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
   337                     jscall = unicode(js.ajaxBoxRemoveLinkedEntity(
   306                         self.__regid__, entity.eid, rentity.eid,
   338                         self.__regid__, entity.eid, rentity.eid,
   307                         self.fname_remove,
   339                         self.fname_remove,
   308                         self.removed_msg and _(self.removed_msg)))
   340                         self.removed_msg and _(self.removed_msg)))
   309                     w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
   341                     w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
   310                       '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
   342                       '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall),
   311                                                            subview))
   343                                                             subview))
   312                 else:
   344                 else:
   313                     w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
   345                     w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
   314             w(u'</table>')
   346             w(u'</table>')
   315         else:
   347         else:
   316             w(_('no related entity'))
   348             w(_('no related entity'))
   317         if mayadd:
   349         if mayadd:
   318             req.add_js('jquery.autocomplete.js')
   350             req.add_js('jquery.autocomplete.js')
   319             req.add_css('jquery.autocomplete.css')
   351             req.add_css('jquery.autocomplete.css')
   320             multiple = rdef.role_cardinality(self.role) in '*+'
   352             multiple = self.rdef.role_cardinality(self.role) in '*+'
   321             w(u'<table><tr><td>')
   353             w(u'<table><tr><td>')
   322             jscall = unicode(js.ajaxBoxShowSelector(
   354             jscall = unicode(js.ajaxBoxShowSelector(
   323                 self.__regid__, entity.eid, self.fname_vocabulary,
   355                 self.__regid__, entity.eid, self.fname_vocabulary,
   324                 self.fname_validate, self.added_msg and _(self.added_msg),
   356                 self.fname_validate, self.added_msg and _(self.added_msg),
   325                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
   357                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
   326                 multiple and self.separator))
   358                 multiple and self.separator))
   327             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
   359             w('<a class="button sglink" href="javascript: %s">%s</a>' % (
   328                 xml_escape(jscall),
   360                 xml_escape(jscall),
   329                 multiple and _('add_relation') or _('update_relation')))
   361                 multiple and _('add_relation') or _('update_relation')))
   330             w(u'</td><td>')
   362             w(u'</td><td>')
   331             w(u'<div id="%sHolder"></div>' % divid)
   363             w(u'<div id="%sHolder"></div>' % self.domid)
   332             w(u'</td></tr></table>')
   364             w(u'</td></tr></table>')
   333         w(u'</div>\n')
   365 
   334         w(u'</div></div>\n')
   366 
       
   367 # old box system, deprecated ###################################################
       
   368 
       
   369 class BoxTemplate(View):
       
   370     """base template for boxes, usually a (contextual) list of possible
       
   371 
       
   372     actions. Various classes attributes may be used to control the box
       
   373     rendering.
       
   374 
       
   375     You may override on of the formatting callbacks is this is not necessary
       
   376     for your custom box.
       
   377 
       
   378     Classes inheriting from this class usually only have to override call
       
   379     to fetch desired actions, and then to do something like  ::
       
   380 
       
   381         box.render(self.w)
       
   382     """
       
   383     __metaclass__ = class_deprecated
       
   384     __deprecation_warning__ = '*BoxTemplate classes are deprecated, use *Box instead'
       
   385 
       
   386     __registry__ = 'boxes'
       
   387     __select__ = ~no_cnx() & match_context_prop()
       
   388 
       
   389     categories_in_order = ()
       
   390     cw_property_defs = {
       
   391         _('visible'): dict(type='Boolean', default=True,
       
   392                            help=_('display the box or not')),
       
   393         _('order'):   dict(type='Int', default=99,
       
   394                            help=_('display order of the box')),
       
   395         # XXX 'incontext' boxes are handled by the default primary view
       
   396         _('context'): dict(type='String', default='left',
       
   397                            vocabulary=(_('left'), _('incontext'), _('right')),
       
   398                            help=_('context where this box should be displayed')),
       
   399         }
       
   400     context = 'left'
       
   401 
       
   402     def sort_actions(self, actions):
       
   403         """return a list of (category, actions_sorted_by_title)"""
       
   404         return sort_by_category(actions, self.categories_in_order)
       
   405 
       
   406     def mk_action(self, title, url, escape=True, **kwargs):
       
   407         """factory function to create dummy actions compatible with the
       
   408         .format_actions method
       
   409         """
       
   410         if escape:
       
   411             title = xml_escape(title)
       
   412         return self.box_action(self._action(title, url, **kwargs))
       
   413 
       
   414     def _action(self, title, url, **kwargs):
       
   415         return UnregisteredAction(self._cw, title, url, **kwargs)
       
   416 
       
   417     # formating callbacks
       
   418 
       
   419     def boxitem_link_tooltip(self, action):
       
   420         if action.__regid__:
       
   421             return u'keyword: %s' % action.__regid__
       
   422         return u''
       
   423 
       
   424     def box_action(self, action):
       
   425         klass = getattr(action, 'html_class', lambda: None)()
       
   426         return BoxLink(action.url(), self._cw._(action.title),
       
   427                        klass, self.boxitem_link_tooltip(action))
       
   428 
       
   429 
       
   430 class RQLBoxTemplate(BoxTemplate):
       
   431     """abstract box for boxes displaying the content of a rql query not
       
   432     related to the current result set.
       
   433     """
       
   434 
       
   435     rql  = None
       
   436 
       
   437     def to_display_rql(self):
       
   438         assert self.rql is not None, self.__regid__
       
   439         return (self.rql,)
       
   440 
       
   441     def call(self, **kwargs):
       
   442         try:
       
   443             rset = self._cw.execute(*self.to_display_rql())
       
   444         except Unauthorized:
       
   445             # can't access to something in the query, forget this box
       
   446             return
       
   447         if len(rset) == 0:
       
   448             return
       
   449         box = BoxWidget(self._cw._(self.title), self.__regid__)
       
   450         for i, (teid, tname) in enumerate(rset):
       
   451             entity = rset.get_entity(i, 0)
       
   452             box.append(self.mk_action(tname, entity.absolute_url()))
       
   453         box.render(w=self.w)
       
   454 
       
   455 
       
   456 class UserRQLBoxTemplate(RQLBoxTemplate):
       
   457     """same as rql box template but the rql is build using the eid of the
       
   458     request's user
       
   459     """
       
   460 
       
   461     def to_display_rql(self):
       
   462         assert self.rql is not None, self.__regid__
       
   463         return (self.rql, {'x': self._cw.user.eid})
       
   464 
       
   465 
       
   466 class EntityBoxTemplate(BoxTemplate):
       
   467     """base class for boxes related to a single entity"""
       
   468     __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
       
   469     context = 'incontext'
       
   470 
       
   471     def call(self, row=0, col=0, **kwargs):
       
   472         """classes inheriting from EntityBoxTemplate should define cell_call"""
       
   473         self.cell_call(row, col, **kwargs)
       
   474 
       
   475 
       
   476 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
       
   477     """base class for boxes which let add or remove entities linked
       
   478     by a given relation
       
   479 
       
   480     subclasses should define at least id, rtype and target
       
   481     class attributes.
       
   482     """
       
   483 
       
   484     def cell_call(self, row, col, view=None, **kwargs):
       
   485         self._cw.add_js('cubicweb.ajax.js')
       
   486         entity = self.cw_rset.get_entity(row, col)
       
   487         title = display_name(self._cw, self.rtype, get_role(self),
       
   488                              context=entity.__regid__)
       
   489         box = SideBoxWidget(title, self.__regid__)
       
   490         related = self.related_boxitems(entity)
       
   491         unrelated = self.unrelated_boxitems(entity)
       
   492         box.extend(related)
       
   493         if related and unrelated:
       
   494             box.append(BoxSeparator())
       
   495         box.extend(unrelated)
       
   496         box.render(self.w)
       
   497 
       
   498     def box_item(self, entity, etarget, rql, label):
       
   499         label = super(EditRelationBoxTemplate, self).box_item(
       
   500             entity, etarget, rql, label)
       
   501         return RawBoxItem(label, liclass=u'invisible')
       
   502 
       
   503 
       
   504 AjaxEditRelationBoxTemplate = class_renamed(
       
   505     'AjaxEditRelationBoxTemplate', AjaxEditRelationBox,
       
   506     '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationBox')
       
   507