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