diff -r 000000000000 -r b97547f5f1fa web/box.py --- /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'[%s] %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) +