1 # copyright 2003-2011 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 """Generic boxes for CubicWeb web client: |
|
19 |
|
20 * actions box |
|
21 * search box |
|
22 |
|
23 Additional boxes (disabled by default): |
|
24 * schema box |
|
25 * possible views box |
|
26 * startup views box |
|
27 """ |
|
28 __docformat__ = "restructuredtext en" |
|
29 from cubicweb import _ |
|
30 |
|
31 from warnings import warn |
|
32 |
|
33 from six import text_type, add_metaclass |
|
34 |
|
35 from logilab.mtconverter import xml_escape |
|
36 from logilab.common.deprecation import class_deprecated |
|
37 |
|
38 from cubicweb import Unauthorized |
|
39 from cubicweb.predicates import (match_user_groups, match_kwargs, |
|
40 non_final_entity, nonempty_rset, |
|
41 match_context, contextual) |
|
42 from cubicweb.utils import wrap_on_write |
|
43 from cubicweb.view import EntityView |
|
44 from cubicweb.schema import display_name |
|
45 from cubicweb.web import component, box, htmlwidgets |
|
46 |
|
47 # XXX bw compat, some cubes import this class from here |
|
48 BoxTemplate = box.BoxTemplate |
|
49 BoxHtml = htmlwidgets.BoxHtml |
|
50 |
|
51 class EditBox(component.CtxComponent): |
|
52 """ |
|
53 box with all actions impacting the entity displayed: edit, copy, delete |
|
54 change state, add related entities... |
|
55 """ |
|
56 __regid__ = 'edit_box' |
|
57 |
|
58 title = _('actions') |
|
59 order = 2 |
|
60 contextual = True |
|
61 __select__ = component.CtxComponent.__select__ & non_final_entity() |
|
62 |
|
63 def init_rendering(self): |
|
64 super(EditBox, self).init_rendering() |
|
65 _ = self._cw._ |
|
66 self._menus_in_order = [] |
|
67 self._menus_by_id = {} |
|
68 # build list of actions |
|
69 actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset, |
|
70 **self.cw_extra_kwargs) |
|
71 other_menu = self._get_menu('moreactions', _('more actions')) |
|
72 for category, defaultmenu in (('mainactions', self), |
|
73 ('moreactions', other_menu), |
|
74 ('addrelated', None)): |
|
75 for action in actions.get(category, ()): |
|
76 if action.submenu: |
|
77 menu = self._get_menu(action.submenu) |
|
78 else: |
|
79 menu = defaultmenu |
|
80 action.fill_menu(self, menu) |
|
81 # if we've nothing but actions in the other_menu, add them directly into the box |
|
82 if not self.items and len(self._menus_by_id) == 1 and not other_menu.is_empty(): |
|
83 self.items = other_menu.items |
|
84 else: # ensure 'more actions' menu appears last |
|
85 self._menus_in_order.remove(other_menu) |
|
86 self._menus_in_order.append(other_menu) |
|
87 for submenu in self._menus_in_order: |
|
88 self.add_submenu(self, submenu) |
|
89 if not self.items: |
|
90 raise component.EmptyComponent() |
|
91 |
|
92 def render_title(self, w): |
|
93 title = self._cw._(self.title) |
|
94 if self.cw_rset: |
|
95 etypes = self.cw_rset.column_types(0) |
|
96 if len(etypes) == 1: |
|
97 plural = self.cw_rset.rowcount > 1 and 'plural' or '' |
|
98 etypelabel = display_name(self._cw, next(iter(etypes)), plural) |
|
99 title = u'%s - %s' % (title, etypelabel.lower()) |
|
100 w(title) |
|
101 |
|
102 def render_body(self, w): |
|
103 self.render_items(w) |
|
104 |
|
105 def _get_menu(self, id, title=None, label_prefix=None): |
|
106 try: |
|
107 return self._menus_by_id[id] |
|
108 except KeyError: |
|
109 if title is None: |
|
110 title = self._cw._(id) |
|
111 self._menus_by_id[id] = menu = htmlwidgets.BoxMenu(title) |
|
112 menu.label_prefix = label_prefix |
|
113 self._menus_in_order.append(menu) |
|
114 return menu |
|
115 |
|
116 def add_submenu(self, box, submenu, label_prefix=None): |
|
117 appendanyway = getattr(submenu, 'append_anyway', False) |
|
118 if len(submenu.items) == 1 and not appendanyway: |
|
119 boxlink = submenu.items[0] |
|
120 if submenu.label_prefix: |
|
121 # XXX iirk |
|
122 if hasattr(boxlink, 'label'): |
|
123 boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label) |
|
124 else: |
|
125 boxlink = u'%s %s' % (submenu.label_prefix, boxlink) |
|
126 box.append(boxlink) |
|
127 elif submenu.items: |
|
128 box.append(submenu) |
|
129 elif appendanyway: |
|
130 box.append(xml_escape(submenu.label)) |
|
131 |
|
132 |
|
133 class SearchBox(component.CtxComponent): |
|
134 """display a box with a simple search form""" |
|
135 __regid__ = 'search_box' |
|
136 |
|
137 title = _('search') |
|
138 order = 0 |
|
139 formdef = u"""<form action="%(action)s"> |
|
140 <table id="%(id)s"><tr><td> |
|
141 <input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" /> |
|
142 <input type="hidden" name="__fromsearchbox" value="1" /> |
|
143 <input type="hidden" name="subvid" value="tsearch" /> |
|
144 </td><td> |
|
145 <input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" /> |
|
146 </td></tr></table> |
|
147 </form>""" |
|
148 |
|
149 def render_title(self, w): |
|
150 w(u"""<span onclick="javascript: toggleVisibility('rqlinput')">%s</span>""" |
|
151 % self._cw._(self.title)) |
|
152 |
|
153 def render_body(self, w): |
|
154 if self._cw.form.pop('__fromsearchbox', None): |
|
155 rql = self._cw.form.get('rql', '') |
|
156 else: |
|
157 rql = '' |
|
158 tabidx1 = self._cw.next_tabindex() |
|
159 tabidx2 = self._cw.next_tabindex() |
|
160 w(self.formdef % {'action': self._cw.build_url('view'), |
|
161 'value': xml_escape(rql), |
|
162 'id': self.cw_extra_kwargs.get('domid', 'tsearch'), |
|
163 'tabindex1': tabidx1, |
|
164 'tabindex2': tabidx2}) |
|
165 |
|
166 |
|
167 # boxes disabled by default ################################################### |
|
168 |
|
169 class PossibleViewsBox(component.CtxComponent): |
|
170 """display a box containing links to all possible views""" |
|
171 __regid__ = 'possible_views_box' |
|
172 |
|
173 contextual = True |
|
174 title = _('possible views') |
|
175 order = 10 |
|
176 visible = False # disabled by default |
|
177 |
|
178 def init_rendering(self): |
|
179 self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw, |
|
180 rset=self.cw_rset) |
|
181 if v.category != 'startupview'] |
|
182 if not self.views: |
|
183 raise component.EmptyComponent() |
|
184 self.items = [] |
|
185 |
|
186 def render_body(self, w): |
|
187 for category, views in box.sort_by_category(self.views): |
|
188 menu = htmlwidgets.BoxMenu(self._cw._(category), ident=category) |
|
189 for view in views: |
|
190 menu.append(self.action_link(view)) |
|
191 self.append(menu) |
|
192 self.render_items(w) |
|
193 |
|
194 |
|
195 class StartupViewsBox(PossibleViewsBox): |
|
196 """display a box containing links to all startup views""" |
|
197 __regid__ = 'startup_views_box' |
|
198 |
|
199 contextual = False |
|
200 title = _('startup views') |
|
201 order = 70 |
|
202 visible = False # disabled by default |
|
203 |
|
204 def init_rendering(self): |
|
205 self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw) |
|
206 if v.category == 'startupview'] |
|
207 if not self.views: |
|
208 raise component.EmptyComponent() |
|
209 self.items = [] |
|
210 |
|
211 |
|
212 class RsetBox(component.CtxComponent): |
|
213 """helper view class to display an rset in a sidebox""" |
|
214 __select__ = nonempty_rset() & match_kwargs('title', 'vid') |
|
215 __regid__ = 'rsetbox' |
|
216 cw_property_defs = {} |
|
217 context = 'incontext' |
|
218 |
|
219 @property |
|
220 def domid(self): |
|
221 return super(RsetBox, self).domid + text_type(abs(id(self))) + text_type(abs(id(self.cw_rset))) |
|
222 |
|
223 def render_title(self, w): |
|
224 w(self.cw_extra_kwargs['title']) |
|
225 |
|
226 def render_body(self, w): |
|
227 if 'dispctrl' in self.cw_extra_kwargs: |
|
228 # XXX do not modify dispctrl! |
|
229 self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext') |
|
230 self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1) |
|
231 self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w, |
|
232 initargs=self.cw_extra_kwargs) |
|
233 |
|
234 # helper classes ############################################################## |
|
235 |
|
236 @add_metaclass(class_deprecated) |
|
237 class SideBoxView(EntityView): |
|
238 """helper view class to display some entities in a sidebox""" |
|
239 __deprecation_warning__ = '[3.10] SideBoxView is deprecated, use RsetBox instead (%(cls)s)' |
|
240 |
|
241 __regid__ = 'sidebox' |
|
242 |
|
243 def call(self, title=u'', **kwargs): |
|
244 """display a list of entities by calling their <item_vid> view""" |
|
245 if 'dispctrl' in self.cw_extra_kwargs: |
|
246 # XXX do not modify dispctrl! |
|
247 self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext') |
|
248 self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1) |
|
249 if title: |
|
250 self.cw_extra_kwargs['title'] = title |
|
251 self.cw_extra_kwargs.setdefault('context', 'incontext') |
|
252 box = self._cw.vreg['ctxcomponents'].select( |
|
253 'rsetbox', self._cw, rset=self.cw_rset, vid='autolimited', |
|
254 **self.cw_extra_kwargs) |
|
255 box.render(self.w) |
|
256 |
|
257 |
|
258 class ContextualBoxLayout(component.Layout): |
|
259 __select__ = match_context('incontext', 'left', 'right') & contextual() |
|
260 # predefined class in cubicweb.css: contextualBox | contextFreeBox |
|
261 cssclass = 'contextualBox' |
|
262 |
|
263 def render(self, w): |
|
264 if self.init_rendering(): |
|
265 view = self.cw_extra_kwargs['view'] |
|
266 w(u'<div class="%s %s" id="%s">' % (self.cssclass, view.cssclass, |
|
267 view.domid)) |
|
268 with wrap_on_write(w, u'<div class="boxTitle"><span>', |
|
269 u'</span></div>') as wow: |
|
270 view.render_title(wow) |
|
271 w(u'<div class="boxBody">') |
|
272 view.render_body(w) |
|
273 # boxFooter div is a CSS place holder (for shadow for example) |
|
274 w(u'</div><div class="boxFooter"></div></div>\n') |
|
275 |
|
276 |
|
277 class ContextFreeBoxLayout(ContextualBoxLayout): |
|
278 __select__ = match_context('incontext', 'left', 'right') & ~contextual() |
|
279 cssclass = 'contextFreeBox' |
|