web/box.py
changeset 0 b97547f5f1fa
child 175 5c7bb5f1ede0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/box.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,238 @@
+"""abstract box classes for CubicWeb web client
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.decorators import cached
+from logilab.mtconverter import html_escape
+
+from cubicweb import Unauthorized
+from cubicweb.common.registerers import (accepts_registerer,
+                                      extresources_registerer,
+                                      etype_rtype_priority_registerer)
+from cubicweb.common.selectors import (etype_rtype_selector, onelinerset_selector,
+                                    accept_selector, accept_rtype_selector,
+                                    primaryview_selector, contextprop_selector)
+from cubicweb.common.view import Template
+from cubicweb.common.appobject import ReloadableMixIn
+
+from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
+                                   RawBoxItem, BoxSeparator)
+from cubicweb.web.action import UnregisteredAction
+
+_ = unicode
+
+
+class BoxTemplate(Template):
+    """base template for boxes, usually a (contextual) list of possible
+    
+    actions. Various classes attributes may be used to control the box
+    rendering.
+    
+    You may override on of the formatting callbacks is this is not necessary
+    for your custom box.
+    
+    Classes inheriting from this class usually only have to override call
+    to fetch desired actions, and then to do something like  ::
+
+        box.render(self.w)
+    """
+    __registry__ = 'boxes'
+    __selectors__ = Template.__selectors__ + (contextprop_selector,)
+    
+    categories_in_order = ()
+    property_defs = {
+        _('visible'): dict(type='Boolean', default=True,
+                           help=_('display the box or not')),
+        _('order'):   dict(type='Int', default=99,
+                           help=_('display order of the box')),
+        # XXX 'incontext' boxes are handled by the default primary view
+        _('context'): dict(type='String', default='left',
+                           vocabulary=(_('left'), _('incontext'), _('right')),
+                           help=_('context where this box should be displayed')),
+        }
+    context = 'left'
+    htmlitemclass = 'boxItem'
+
+    def sort_actions(self, actions):
+        """return a list of (category, actions_sorted_by_title)"""
+        result = []
+        actions_by_cat = {}
+        for action in actions:
+            actions_by_cat.setdefault(action.category, []).append((action.title, action))
+        for key, values in actions_by_cat.items():
+            actions_by_cat[key] = [act for title, act in sorted(values)]
+        for cat in self.categories_in_order:
+            if cat in actions_by_cat:
+                result.append( (cat, actions_by_cat[cat]) )
+        for item in sorted(actions_by_cat.items()):
+            result.append(item)
+        return result
+
+    def mk_action(self, title, path, escape=True, **kwargs):
+        """factory function to create dummy actions compatible with the
+        .format_actions method
+        """
+        if escape:
+            title = html_escape(title)
+        return self.box_action(self._action(title, path, **kwargs))
+    
+    def _action(self, title, path, **kwargs):
+        return UnregisteredAction(self.req, self.rset, title, path, **kwargs)        
+
+    # formating callbacks
+
+    def boxitem_link_tooltip(self, action):
+        if action.id:
+            return u'keyword: %s' % action.id
+        return u''
+
+    def box_action(self, action):
+        cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass
+        return BoxLink(action.url(), self.req._(action.title),
+                       cls, self.boxitem_link_tooltip(action))
+        
+
+class RQLBoxTemplate(BoxTemplate):
+    """abstract box for boxes displaying the content of a rql query not
+    related to the current result set.
+    
+    It rely on etype, rtype (both optional, usable to control registration
+    according to application schema and display according to connected
+    user's rights) and rql attributes
+    """
+    __registerer__ = etype_rtype_priority_registerer
+    __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,)
+
+    rql  = None
+    
+    def to_display_rql(self):
+        assert self.rql is not None, self.id
+        return (self.rql,)
+    
+    def call(self, **kwargs):
+        try:
+            rset = self.req.execute(*self.to_display_rql())
+        except Unauthorized:
+            # can't access to something in the query, forget this box
+            return
+        if len(rset) == 0:
+            return
+        box = BoxWidget(self.req._(self.title), self.id)
+        for i, (teid, tname) in enumerate(rset):
+            entity = rset.get_entity(i, 0)
+            box.append(self.mk_action(tname, entity.absolute_url()))
+        box.render(w=self.w)
+
+        
+class UserRQLBoxTemplate(RQLBoxTemplate):
+    """same as rql box template but the rql is build using the eid of the
+    request's user
+    """
+
+    def to_display_rql(self):
+        assert self.rql is not None, self.id
+        return (self.rql, {'x': self.req.user.eid}, 'x')
+    
+
+class ExtResourcesBoxTemplate(BoxTemplate):
+    """base class for boxes displaying external resources such as the RSS logo.
+    It should list necessary resources with the .need_resources attribute.
+    """
+    __registerer__ = extresources_registerer
+    need_resources = ()
+
+
+class EntityBoxTemplate(BoxTemplate):
+    """base class for boxes related to a single entity"""
+    __registerer__ = accepts_registerer
+    __selectors__ = (onelinerset_selector, primaryview_selector,
+                     contextprop_selector, etype_rtype_selector,
+                     accept_rtype_selector, accept_selector)
+    accepts = ('Any',)
+    context = 'incontext'
+    
+    def call(self, row=0, col=0, **kwargs):
+        """classes inheriting from EntityBoxTemplate should defined cell_call,
+        """
+        self.cell_call(row, col, **kwargs)
+
+
+
+class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate):
+    """base class for boxes which let add or remove entities linked
+    by a given relation
+
+    subclasses should define at least id, rtype and target
+    class attributes.
+    """
+    
+    def cell_call(self, row, col):
+        self.req.add_js('cubicweb.ajax.js')
+        entity = self.entity(row, col)
+        box = SideBoxWidget(display_name(self.req, self.rtype), self.id)
+        count = self.w_related(box, entity)
+        if count:
+            box.append(BoxSeparator())
+        self.w_unrelated(box, entity)
+        box.render(self.w)
+
+    def div_id(self):
+        return self.id
+
+    @cached
+    def xtarget(self):
+        if self.target == 'subject':
+            return 'object', 'subject'
+        return 'subject', 'object'
+        
+    def box_item(self, entity, etarget, rql, label):
+        """builds HTML link to edit relation between `entity` and `etarget`
+        """
+        x, target = self.xtarget()
+        args = {x[0] : entity.eid, target[0] : etarget.eid}
+        url = self.user_rql_callback((rql, args))
+        # for each target, provide a link to edit the relation
+        label = u'[<a href="%s">%s</a>] %s' % (url, label,
+                                               etarget.view('incontext'))
+        return RawBoxItem(label, liclass=u'invisible')
+    
+    def w_related(self, box, entity):
+        """appends existing relations to the `box`"""
+        rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        related = self.related_entities(entity)
+        for etarget in related:
+            box.append(self.box_item(entity, etarget, rql, u'-'))
+        return len(related)
+    
+    def w_unrelated(self, box, entity):
+        """appends unrelated entities to the `box`"""
+        rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype
+        for etarget in self.unrelated_entities(entity):
+            box.append(self.box_item(entity, etarget, rql, u'+'))
+
+    def unrelated_entities(self, entity):
+        """returns the list of unrelated entities
+
+        if etype is not defined on the Box's class, the default
+        behaviour is to use the entity's appropraite vocabulary function
+        """
+        x, target = self.xtarget()
+        # use entity.unrelated if we've been asked for a particular etype
+        if hasattr(self, 'etype'):
+            return entity.unrelated(self.rtype, self.etype, x).entities()
+        # in other cases, use vocabulary functions
+        entities = []
+        for _, eid in entity.vocabulary(self.rtype, x):
+            if eid is not None:
+                rset = self.req.eid_rset(eid)
+                entities.append(rset.get_entity(0, 0))
+        return entities
+        
+    def related_entities(self, entity):
+        x, target = self.xtarget()
+        return entity.related(self.rtype, x, entities=True)
+