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