web/box.py
changeset 1808 aa09e20dd8c0
parent 1696 ee0bea49e0e1
child 1885 c2011d238e98
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
     1 """abstract box classes for CubicWeb web client
     1 """abstract box classes for CubicWeb web client
     2 
     2 
     3 :organization: Logilab
     3 :organization: Logilab
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     6 """
     6 """
     7 __docformat__ = "restructuredtext en"
     7 __docformat__ = "restructuredtext en"
     8 
     8 
     9 from logilab.common.decorators import cached
       
    10 from logilab.mtconverter import html_escape
     9 from logilab.mtconverter import html_escape
    11 
    10 
    12 from cubicweb import Unauthorized, role as get_role
    11 from cubicweb import Unauthorized, role as get_role, target as get_target
    13 from cubicweb.common.registerers import (
    12 from cubicweb.selectors import (one_line_rset,  primary_view,
    14     accepts_registerer, extresources_registerer,
    13                                 match_context_prop, partial_has_related_entities,
    15     etype_rtype_priority_registerer)
    14                                 accepts_compat, has_relation_compat,
    16 from cubicweb.common.selectors import (
    15                                 condition_compat, require_group_compat)
    17     etype_rtype_selector, one_line_rset, accept, has_relation,
    16 from cubicweb.view import View, ReloadableMixIn
    18     primary_view, match_context_prop, has_related_entities,
       
    19     _rql_condition)
       
    20 from cubicweb.common.view import Template
       
    21 from cubicweb.common.appobject import ReloadableMixIn
       
    22 
    17 
    23 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    18 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
    24                                       RawBoxItem, BoxSeparator)
    19                                       RawBoxItem, BoxSeparator)
    25 from cubicweb.web.action import UnregisteredAction
    20 from cubicweb.web.action import UnregisteredAction
    26 
    21 
    27 _ = unicode
    22 _ = unicode
    28 
    23 
    29 
    24 
    30 class BoxTemplate(Template):
    25 class BoxTemplate(View):
    31     """base template for boxes, usually a (contextual) list of possible
    26     """base template for boxes, usually a (contextual) list of possible
    32     
    27 
    33     actions. Various classes attributes may be used to control the box
    28     actions. Various classes attributes may be used to control the box
    34     rendering.
    29     rendering.
    35     
    30 
    36     You may override on of the formatting callbacks is this is not necessary
    31     You may override on of the formatting callbacks is this is not necessary
    37     for your custom box.
    32     for your custom box.
    38     
    33 
    39     Classes inheriting from this class usually only have to override call
    34     Classes inheriting from this class usually only have to override call
    40     to fetch desired actions, and then to do something like  ::
    35     to fetch desired actions, and then to do something like  ::
    41 
    36 
    42         box.render(self.w)
    37         box.render(self.w)
    43     """
    38     """
    44     __registry__ = 'boxes'
    39     __registry__ = 'boxes'
    45     __selectors__ = Template.__selectors__ + (match_context_prop,)
    40     __select__ = match_context_prop()
    46     
    41     registered = classmethod(require_group_compat(View.registered))
       
    42 
    47     categories_in_order = ()
    43     categories_in_order = ()
    48     property_defs = {
    44     property_defs = {
    49         _('visible'): dict(type='Boolean', default=True,
    45         _('visible'): dict(type='Boolean', default=True,
    50                            help=_('display the box or not')),
    46                            help=_('display the box or not')),
    51         _('order'):   dict(type='Int', default=99,
    47         _('order'):   dict(type='Int', default=99,
    78         .format_actions method
    74         .format_actions method
    79         """
    75         """
    80         if escape:
    76         if escape:
    81             title = html_escape(title)
    77             title = html_escape(title)
    82         return self.box_action(self._action(title, path, **kwargs))
    78         return self.box_action(self._action(title, path, **kwargs))
    83     
    79 
    84     def _action(self, title, path, **kwargs):
    80     def _action(self, title, path, **kwargs):
    85         return UnregisteredAction(self.req, self.rset, title, path, **kwargs)        
    81         return UnregisteredAction(self.req, self.rset, title, path, **kwargs)
    86 
    82 
    87     # formating callbacks
    83     # formating callbacks
    88 
    84 
    89     def boxitem_link_tooltip(self, action):
    85     def boxitem_link_tooltip(self, action):
    90         if action.id:
    86         if action.id:
    93 
    89 
    94     def box_action(self, action):
    90     def box_action(self, action):
    95         cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
    91         cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
    96         return BoxLink(action.url(), self.req._(action.title),
    92         return BoxLink(action.url(), self.req._(action.title),
    97                        cls, self.boxitem_link_tooltip(action))
    93                        cls, self.boxitem_link_tooltip(action))
    98         
    94 
    99 
    95 
   100 class RQLBoxTemplate(BoxTemplate):
    96 class RQLBoxTemplate(BoxTemplate):
   101     """abstract box for boxes displaying the content of a rql query not
    97     """abstract box for boxes displaying the content of a rql query not
   102     related to the current result set.
    98     related to the current result set.
   103     
    99 
   104     It rely on etype, rtype (both optional, usable to control registration
   100     It rely on etype, rtype (both optional, usable to control registration
   105     according to application schema and display according to connected
   101     according to application schema and display according to connected
   106     user's rights) and rql attributes
   102     user's rights) and rql attributes
   107     """
   103     """
   108     __registerer__ = etype_rtype_priority_registerer
   104 #XXX    __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,)
   109     __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,)
       
   110 
   105 
   111     rql  = None
   106     rql  = None
   112     
   107 
   113     def to_display_rql(self):
   108     def to_display_rql(self):
   114         assert self.rql is not None, self.id
   109         assert self.rql is not None, self.id
   115         return (self.rql,)
   110         return (self.rql,)
   116     
   111 
   117     def call(self, **kwargs):
   112     def call(self, **kwargs):
   118         try:
   113         try:
   119             rset = self.req.execute(*self.to_display_rql())
   114             rset = self.req.execute(*self.to_display_rql())
   120         except Unauthorized:
   115         except Unauthorized:
   121             # can't access to something in the query, forget this box
   116             # can't access to something in the query, forget this box
   126         for i, (teid, tname) in enumerate(rset):
   121         for i, (teid, tname) in enumerate(rset):
   127             entity = rset.get_entity(i, 0)
   122             entity = rset.get_entity(i, 0)
   128             box.append(self.mk_action(tname, entity.absolute_url()))
   123             box.append(self.mk_action(tname, entity.absolute_url()))
   129         box.render(w=self.w)
   124         box.render(w=self.w)
   130 
   125 
   131         
   126 
   132 class UserRQLBoxTemplate(RQLBoxTemplate):
   127 class UserRQLBoxTemplate(RQLBoxTemplate):
   133     """same as rql box template but the rql is build using the eid of the
   128     """same as rql box template but the rql is build using the eid of the
   134     request's user
   129     request's user
   135     """
   130     """
   136 
   131 
   137     def to_display_rql(self):
   132     def to_display_rql(self):
   138         assert self.rql is not None, self.id
   133         assert self.rql is not None, self.id
   139         return (self.rql, {'x': self.req.user.eid}, 'x')
   134         return (self.rql, {'x': self.req.user.eid}, 'x')
   140     
       
   141 
       
   142 class ExtResourcesBoxTemplate(BoxTemplate):
       
   143     """base class for boxes displaying external resources such as the RSS logo.
       
   144     It should list necessary resources with the .need_resources attribute.
       
   145     """
       
   146     __registerer__ = extresources_registerer
       
   147     need_resources = ()
       
   148 
   135 
   149 
   136 
   150 class EntityBoxTemplate(BoxTemplate):
   137 class EntityBoxTemplate(BoxTemplate):
   151     """base class for boxes related to a single entity"""
   138     """base class for boxes related to a single entity"""
   152     __registerer__ = accepts_registerer
   139     __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
   153     __selectors__ = (one_line_rset, primary_view,
   140     registered = accepts_compat(has_relation_compat(condition_compat(BoxTemplate.registered)))
   154                      match_context_prop, etype_rtype_selector,
       
   155                      has_relation, accept, _rql_condition)
       
   156     accepts = ('Any',)
       
   157     context = 'incontext'
   141     context = 'incontext'
   158     condition = None
   142 
   159     
       
   160     def call(self, row=0, col=0, **kwargs):
   143     def call(self, row=0, col=0, **kwargs):
   161         """classes inheriting from EntityBoxTemplate should define cell_call"""
   144         """classes inheriting from EntityBoxTemplate should define cell_call"""
   162         self.cell_call(row, col, **kwargs)
   145         self.cell_call(row, col, **kwargs)
   163 
   146 
   164 
   147 
   165 class RelatedEntityBoxTemplate(EntityBoxTemplate):
   148 class RelatedEntityBoxTemplate(EntityBoxTemplate):
   166     __selectors__ = EntityBoxTemplate.__selectors__ + (has_related_entities,)
   149     __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
   167     
   150 
   168     def cell_call(self, row, col, **kwargs):
   151     def cell_call(self, row, col, **kwargs):
   169         entity = self.entity(row, col)
   152         entity = self.entity(row, col)
   170         limit = self.req.property_value('navigation.related-limit') + 1
   153         limit = self.req.property_value('navigation.related-limit') + 1
   171         role = get_role(self)
   154         role = get_role(self)
   172         self.w(u'<div class="sideRelated">')
   155         self.w(u'<div class="sideRelated">')
   180     by a given relation
   163     by a given relation
   181 
   164 
   182     subclasses should define at least id, rtype and target
   165     subclasses should define at least id, rtype and target
   183     class attributes.
   166     class attributes.
   184     """
   167     """
   185     
   168 
   186     def cell_call(self, row, col, view=None):
   169     def cell_call(self, row, col, view=None, **kwargs):
   187         self.req.add_js('cubicweb.ajax.js')
   170         self.req.add_js('cubicweb.ajax.js')
   188         entity = self.entity(row, col)
   171         entity = self.entity(row, col)
   189         box = SideBoxWidget(display_name(self.req, self.rtype), self.id)
   172         box = SideBoxWidget(display_name(self.req, self.rtype), self.id)
   190         count = self.w_related(box, entity)
   173         count = self.w_related(box, entity)
   191         if count:
   174         if count:
   192             box.append(BoxSeparator())
   175             box.append(BoxSeparator())
   193         self.w_unrelated(box, entity)
   176         if not self.w_unrelated(box, entity):
       
   177             del box.items[-1] # remove useless separator
   194         box.render(self.w)
   178         box.render(self.w)
   195 
   179 
   196     def div_id(self):
   180     def div_id(self):
   197         return self.id
   181         return self.id
   198 
   182 
   199     @cached
       
   200     def xtarget(self):
       
   201         if self.target == 'subject':
       
   202             return 'object', 'subject'
       
   203         return 'subject', 'object'
       
   204         
       
   205     def box_item(self, entity, etarget, rql, label):
   183     def box_item(self, entity, etarget, rql, label):
   206         """builds HTML link to edit relation between `entity` and `etarget`
   184         """builds HTML link to edit relation between `entity` and `etarget`
   207         """
   185         """
   208         x, target = self.xtarget()
   186         role, target = get_role(self), get_target(self)
   209         args = {x[0] : entity.eid, target[0] : etarget.eid}
   187         args = {role[0] : entity.eid, target[0] : etarget.eid}
   210         url = self.user_rql_callback((rql, args))
   188         url = self.user_rql_callback((rql, args))
   211         # for each target, provide a link to edit the relation
   189         # for each target, provide a link to edit the relation
   212         label = u'[<a href="%s">%s</a>] %s' % (url, label,
   190         label = u'[<a href="%s">%s</a>] %s' % (url, label,
   213                                                etarget.view('incontext'))
   191                                                etarget.view('incontext'))
   214         return RawBoxItem(label, liclass=u'invisible')
   192         return RawBoxItem(label, liclass=u'invisible')
   215     
   193 
   216     def w_related(self, box, entity):
   194     def w_related(self, box, entity):
   217         """appends existing relations to the `box`"""
   195         """appends existing relations to the `box`"""
   218         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
   196         rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
   219         related = self.related_entities(entity)
   197         related = self.related_entities(entity)
   220         for etarget in related:
   198         for etarget in related:
   221             box.append(self.box_item(entity, etarget, rql, u'-'))
   199             box.append(self.box_item(entity, etarget, rql, u'-'))
   222         return len(related)
   200         return len(related)
   223     
   201 
   224     def w_unrelated(self, box, entity):
   202     def w_unrelated(self, box, entity):
   225         """appends unrelated entities to the `box`"""
   203         """appends unrelated entities to the `box`"""
   226         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
   204         rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
       
   205         i = 0
   227         for etarget in self.unrelated_entities(entity):
   206         for etarget in self.unrelated_entities(entity):
   228             box.append(self.box_item(entity, etarget, rql, u'+'))
   207             box.append(self.box_item(entity, etarget, rql, u'+'))
       
   208             i += 1
       
   209         return i
   229 
   210 
   230     def unrelated_entities(self, entity):
   211     def unrelated_entities(self, entity):
   231         """returns the list of unrelated entities
   212         """returns the list of unrelated entities
   232 
   213 
   233         if etype is not defined on the Box's class, the default
   214         if etype is not defined on the Box's class, the default
   234         behaviour is to use the entity's appropraite vocabulary function
   215         behaviour is to use the entity's appropraite vocabulary function
   235         """
   216         """
   236         x, target = self.xtarget()
       
   237         # use entity.unrelated if we've been asked for a particular etype
   217         # use entity.unrelated if we've been asked for a particular etype
   238         if hasattr(self, 'etype'):
   218         if hasattr(self, 'etype'):
   239             return entity.unrelated(self.rtype, self.etype, x).entities()
   219             return entity.unrelated(self.rtype, self.etype, get_role(self)).entities()
   240         # in other cases, use vocabulary functions
   220         # in other cases, use vocabulary functions
   241         entities = []
   221         entities = []
   242         for _, eid in entity.vocabulary(self.rtype, x):
   222         form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
       
   223                                        row=self.row or 0)
       
   224         field = form.field_by_name(self.rtype, get_role(self), entity.e_schema)
       
   225         for _, eid in form.form_field_vocabulary(field):
   243             if eid is not None:
   226             if eid is not None:
   244                 rset = self.req.eid_rset(eid)
   227                 rset = self.req.eid_rset(eid)
   245                 entities.append(rset.get_entity(0, 0))
   228                 entities.append(rset.get_entity(0, 0))
   246         return entities
   229         return entities
   247         
   230 
   248     def related_entities(self, entity):
   231     def related_entities(self, entity):
   249         x, target = self.xtarget()
   232         return entity.related(self.rtype, get_role(self), entities=True)
   250         return entity.related(self.rtype, x, entities=True)
   233 
   251