|
1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """abstract box classes for CubicWeb web client""" |
|
19 |
|
20 __docformat__ = "restructuredtext en" |
|
21 from cubicweb import _ |
|
22 |
|
23 from six import add_metaclass |
|
24 |
|
25 from logilab.mtconverter import xml_escape |
|
26 from logilab.common.deprecation import class_deprecated, class_renamed |
|
27 |
|
28 from cubicweb import Unauthorized, role as get_role |
|
29 from cubicweb.schema import display_name |
|
30 from cubicweb.predicates import no_cnx, one_line_rset |
|
31 from cubicweb.view import View |
|
32 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs |
|
33 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, |
|
34 RawBoxItem, BoxSeparator) |
|
35 from cubicweb.web.action import UnregisteredAction |
|
36 |
|
37 |
|
38 def sort_by_category(actions, categories_in_order=None): |
|
39 """return a list of (category, actions_sorted_by_title)""" |
|
40 result = [] |
|
41 actions_by_cat = {} |
|
42 for action in actions: |
|
43 actions_by_cat.setdefault(action.category, []).append( |
|
44 (action.title, action) ) |
|
45 for key, values in actions_by_cat.items(): |
|
46 actions_by_cat[key] = [act for title, act in sorted(values, key=lambda x: x[0])] |
|
47 if categories_in_order: |
|
48 for cat in categories_in_order: |
|
49 if cat in actions_by_cat: |
|
50 result.append( (cat, actions_by_cat[cat]) ) |
|
51 for item in sorted(actions_by_cat.items()): |
|
52 result.append(item) |
|
53 return result |
|
54 |
|
55 |
|
56 # old box system, deprecated ################################################### |
|
57 |
|
58 @add_metaclass(class_deprecated) |
|
59 class BoxTemplate(View): |
|
60 """base template for boxes, usually a (contextual) list of possible |
|
61 actions. Various classes attributes may be used to control the box |
|
62 rendering. |
|
63 |
|
64 You may override one of the formatting callbacks if this is not necessary |
|
65 for your custom box. |
|
66 |
|
67 Classes inheriting from this class usually only have to override call |
|
68 to fetch desired actions, and then to do something like :: |
|
69 |
|
70 box.render(self.w) |
|
71 """ |
|
72 __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)' |
|
73 |
|
74 __registry__ = 'ctxcomponents' |
|
75 __select__ = ~no_cnx() |
|
76 |
|
77 categories_in_order = () |
|
78 cw_property_defs = { |
|
79 _('visible'): dict(type='Boolean', default=True, |
|
80 help=_('display the box or not')), |
|
81 _('order'): dict(type='Int', default=99, |
|
82 help=_('display order of the box')), |
|
83 # XXX 'incontext' boxes are handled by the default primary view |
|
84 _('context'): dict(type='String', default='left', |
|
85 vocabulary=(_('left'), _('incontext'), _('right')), |
|
86 help=_('context where this box should be displayed')), |
|
87 } |
|
88 context = 'left' |
|
89 |
|
90 def sort_actions(self, actions): |
|
91 """return a list of (category, actions_sorted_by_title)""" |
|
92 return sort_by_category(actions, self.categories_in_order) |
|
93 |
|
94 def mk_action(self, title, url, escape=True, **kwargs): |
|
95 """factory function to create dummy actions compatible with the |
|
96 .format_actions method |
|
97 """ |
|
98 if escape: |
|
99 title = xml_escape(title) |
|
100 return self.box_action(self._action(title, url, **kwargs)) |
|
101 |
|
102 def _action(self, title, url, **kwargs): |
|
103 return UnregisteredAction(self._cw, title, url, **kwargs) |
|
104 |
|
105 # formating callbacks |
|
106 |
|
107 def boxitem_link_tooltip(self, action): |
|
108 if action.__regid__: |
|
109 return u'keyword: %s' % action.__regid__ |
|
110 return u'' |
|
111 |
|
112 def box_action(self, action): |
|
113 klass = getattr(action, 'html_class', lambda: None)() |
|
114 return BoxLink(action.url(), self._cw._(action.title), |
|
115 klass, self.boxitem_link_tooltip(action)) |
|
116 |
|
117 |
|
118 class RQLBoxTemplate(BoxTemplate): |
|
119 """abstract box for boxes displaying the content of a rql query not |
|
120 related to the current result set. |
|
121 """ |
|
122 |
|
123 # to be defined in concrete classes |
|
124 rql = title = None |
|
125 |
|
126 def to_display_rql(self): |
|
127 assert self.rql is not None, self.__regid__ |
|
128 return (self.rql,) |
|
129 |
|
130 def call(self, **kwargs): |
|
131 try: |
|
132 rset = self._cw.execute(*self.to_display_rql()) |
|
133 except Unauthorized: |
|
134 # can't access to something in the query, forget this box |
|
135 return |
|
136 if len(rset) == 0: |
|
137 return |
|
138 box = BoxWidget(self._cw._(self.title), self.__regid__) |
|
139 for i, (teid, tname) in enumerate(rset): |
|
140 entity = rset.get_entity(i, 0) |
|
141 box.append(self.mk_action(tname, entity.absolute_url())) |
|
142 box.render(w=self.w) |
|
143 |
|
144 |
|
145 class UserRQLBoxTemplate(RQLBoxTemplate): |
|
146 """same as rql box template but the rql is build using the eid of the |
|
147 request's user |
|
148 """ |
|
149 |
|
150 def to_display_rql(self): |
|
151 assert self.rql is not None, self.__regid__ |
|
152 return (self.rql, {'x': self._cw.user.eid}) |
|
153 |
|
154 |
|
155 class EntityBoxTemplate(BoxTemplate): |
|
156 """base class for boxes related to a single entity""" |
|
157 __select__ = BoxTemplate.__select__ & one_line_rset() |
|
158 context = 'incontext' |
|
159 |
|
160 def call(self, row=0, col=0, **kwargs): |
|
161 """classes inheriting from EntityBoxTemplate should define cell_call""" |
|
162 self.cell_call(row, col, **kwargs) |
|
163 |
|
164 from cubicweb.web.component import AjaxEditRelationCtxComponent, EditRelationMixIn |
|
165 |
|
166 |
|
167 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate): |
|
168 """base class for boxes which let add or remove entities linked |
|
169 by a given relation |
|
170 |
|
171 subclasses should define at least id, rtype and target |
|
172 class attributes. |
|
173 """ |
|
174 rtype = None |
|
175 def cell_call(self, row, col, view=None, **kwargs): |
|
176 self._cw.add_js('cubicweb.ajax.js') |
|
177 entity = self.cw_rset.get_entity(row, col) |
|
178 title = display_name(self._cw, self.rtype, get_role(self), |
|
179 context=entity.cw_etype) |
|
180 box = SideBoxWidget(title, self.__regid__) |
|
181 related = self.related_boxitems(entity) |
|
182 unrelated = self.unrelated_boxitems(entity) |
|
183 box.extend(related) |
|
184 if related and unrelated: |
|
185 box.append(BoxSeparator()) |
|
186 box.extend(unrelated) |
|
187 box.render(self.w) |
|
188 |
|
189 def box_item(self, entity, etarget, rql, label): |
|
190 label = super(EditRelationBoxTemplate, self).box_item( |
|
191 entity, etarget, rql, label) |
|
192 return RawBoxItem(label, liclass=u'invisible') |
|
193 |
|
194 |
|
195 AjaxEditRelationBoxTemplate = class_renamed( |
|
196 'AjaxEditRelationBoxTemplate', AjaxEditRelationCtxComponent, |
|
197 '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationCtxComponent (%(cls)s)') |