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 """This module contains table views, with the following features that may be |
|
19 provided (depending on the used implementation): |
|
20 |
|
21 * facets filtering |
|
22 * pagination |
|
23 * actions menu |
|
24 * properly sortable content |
|
25 * odd/row/hover line styles |
|
26 |
|
27 The three main implementation are described below. Each implementation is |
|
28 suitable for a particular case, but they each attempt to display tables that |
|
29 looks similar. |
|
30 |
|
31 .. autoclass:: cubicweb.web.views.tableview.RsetTableView |
|
32 :members: |
|
33 |
|
34 .. autoclass:: cubicweb.web.views.tableview.EntityTableView |
|
35 :members: |
|
36 |
|
37 .. autoclass:: cubicweb.web.views.pyviews.PyValTableView |
|
38 :members: |
|
39 |
|
40 All those classes are rendered using a *layout*: |
|
41 |
|
42 .. autoclass:: cubicweb.web.views.tableview.TableLayout |
|
43 :members: |
|
44 |
|
45 There is by default only one table layout, using the 'table_layout' identifier, |
|
46 that is referenced by table views |
|
47 :attr:`cubicweb.web.views.tableview.TableMixIn.layout_id`. If you want to |
|
48 customize the look and feel of your table, you can either replace the default |
|
49 one by yours, having multiple variants with proper selectors, or change the |
|
50 `layout_id` identifier of your table to use your table specific implementation. |
|
51 |
|
52 Notice you can gives options to the layout using a `layout_args` dictionary on |
|
53 your class. |
|
54 |
|
55 If you still can't find a view that suit your needs, you should take a look at the |
|
56 class below that is the common abstract base class for the three views defined |
|
57 above and implement your own class. |
|
58 |
|
59 .. autoclass:: cubicweb.web.views.tableview.TableMixIn |
|
60 :members: |
|
61 """ |
|
62 |
|
63 __docformat__ = "restructuredtext en" |
|
64 from cubicweb import _ |
|
65 |
|
66 from warnings import warn |
|
67 from copy import copy |
|
68 from types import MethodType |
|
69 |
|
70 from six import string_types, add_metaclass, create_bound_method |
|
71 from six.moves import range |
|
72 |
|
73 from logilab.mtconverter import xml_escape |
|
74 from logilab.common.decorators import cachedproperty |
|
75 from logilab.common.deprecation import class_deprecated |
|
76 from logilab.common.registry import yes |
|
77 |
|
78 from cubicweb import NoSelectableObject, tags |
|
79 from cubicweb.predicates import nonempty_rset, match_kwargs, objectify_predicate |
|
80 from cubicweb.schema import display_name |
|
81 from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO |
|
82 from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid |
|
83 from cubicweb.view import EntityView, AnyRsetView |
|
84 from cubicweb.web import jsonize, component |
|
85 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, |
|
86 PopupBoxMenu) |
|
87 |
|
88 |
|
89 @objectify_predicate |
|
90 def unreloadable_table(cls, req, rset=None, |
|
91 displaycols=None, headers=None, cellvids=None, |
|
92 paginate=False, displayactions=False, displayfilter=False, |
|
93 **kwargs): |
|
94 # one may wish to specify one of headers/displaycols/cellvids as long as he |
|
95 # doesn't want pagination nor actions nor facets |
|
96 if not kwargs and (displaycols or headers or cellvids) and not ( |
|
97 displayfilter or displayactions or paginate): |
|
98 return 1 |
|
99 return 0 |
|
100 |
|
101 |
|
102 class TableLayout(component.Component): |
|
103 """The default layout for table. When `render` is called, this will use |
|
104 the API described on :class:`TableMixIn` to feed the generated table. |
|
105 |
|
106 This layout behaviour may be customized using the following attributes / |
|
107 selection arguments: |
|
108 |
|
109 * `cssclass`, a string that should be used as HTML class attribute. Default |
|
110 to "listing". |
|
111 |
|
112 * `needs_css`, the CSS files that should be used together with this |
|
113 table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css'). |
|
114 |
|
115 * `needs_js`, the Javascript files that should be used together with this |
|
116 table. Default to ('jquery.tablesorter.js',) |
|
117 |
|
118 * `display_filter`, tells if the facets filter should be displayed when |
|
119 possible. Allowed values are: |
|
120 - `None`, don't display it |
|
121 - 'top', display it above the table |
|
122 - 'bottom', display it below the table |
|
123 |
|
124 * `display_actions`, tells if a menu for available actions should be |
|
125 displayed when possible (see two following options). Allowed values are: |
|
126 - `None`, don't display it |
|
127 - 'top', display it above the table |
|
128 - 'bottom', display it below the table |
|
129 |
|
130 * `hide_filter`, when true (the default), facets filter will be hidden by |
|
131 default, with an action in the actions menu allowing to show / hide it. |
|
132 |
|
133 * `show_all_option`, when true, a *show all results* link will be displayed |
|
134 below the navigation component. |
|
135 |
|
136 * `add_view_actions`, when true, actions returned by view.table_actions() |
|
137 will be included in the actions menu. |
|
138 |
|
139 * `header_column_idx`, if not `None`, should be a colum index or a set of |
|
140 column index where <th> tags should be generated instead of <td> |
|
141 """ #'# make emacs happier |
|
142 __regid__ = 'table_layout' |
|
143 cssclass = "listing" |
|
144 needs_css = ('cubicweb.tableview.css',) |
|
145 needs_js = () |
|
146 display_filter = None # None / 'top' / 'bottom' |
|
147 display_actions = 'top' # None / 'top' / 'bottom' |
|
148 hide_filter = True |
|
149 show_all_option = True # make navcomp generate a 'show all' results link |
|
150 add_view_actions = False |
|
151 header_column_idx = None |
|
152 enable_sorting = True |
|
153 sortvalue_limit = 10 |
|
154 tablesorter_settings = { |
|
155 'textExtraction': JSString('cw.sortValueExtraction'), |
|
156 'selectorHeaders': "thead tr:first th[class='sortable']", # only plug on the first row |
|
157 } |
|
158 |
|
159 def _setup_tablesorter(self, divid): |
|
160 self._cw.add_css('cubicweb.tablesorter.css') |
|
161 self._cw.add_js('jquery.tablesorter.js') |
|
162 self._cw.add_onload('''$(document).ready(function() { |
|
163 $("#%s table").tablesorter(%s); |
|
164 });''' % (divid, js_dumps(self.tablesorter_settings))) |
|
165 |
|
166 def __init__(self, req, view, **kwargs): |
|
167 super(TableLayout, self).__init__(req, **kwargs) |
|
168 for key, val in list(self.cw_extra_kwargs.items()): |
|
169 if hasattr(self.__class__, key) and not key[0] == '_': |
|
170 setattr(self, key, val) |
|
171 self.cw_extra_kwargs.pop(key) |
|
172 self.view = view |
|
173 if self.header_column_idx is None: |
|
174 self.header_column_idx = frozenset() |
|
175 elif isinstance(self.header_column_idx, int): |
|
176 self.header_column_idx = frozenset( (self.header_column_idx,) ) |
|
177 |
|
178 @cachedproperty |
|
179 def initial_load(self): |
|
180 """We detect a bit heuristically if we are built for the first time or |
|
181 from subsequent calls by the form filter or by the pagination hooks. |
|
182 """ |
|
183 form = self._cw.form |
|
184 return 'fromformfilter' not in form and '__fromnavigation' not in form |
|
185 |
|
186 def render(self, w, **kwargs): |
|
187 assert self.display_filter in (None, 'top', 'bottom'), self.display_filter |
|
188 if self.needs_css: |
|
189 self._cw.add_css(self.needs_css) |
|
190 if self.needs_js: |
|
191 self._cw.add_js(self.needs_js) |
|
192 if self.enable_sorting: |
|
193 self._setup_tablesorter(self.view.domid) |
|
194 # Notice facets form must be rendered **outside** the main div as it |
|
195 # shouldn't be rendered on ajax call subsequent to facet restriction |
|
196 # (hence the 'fromformfilter' parameter added by the form |
|
197 generate_form = self.initial_load |
|
198 if self.display_filter and generate_form: |
|
199 facetsform = self.view.facets_form() |
|
200 else: |
|
201 facetsform = None |
|
202 if facetsform and self.display_filter == 'top': |
|
203 cssclass = u'hidden' if self.hide_filter else u'' |
|
204 facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, |
|
205 divid=self.view.domid) |
|
206 actions = [] |
|
207 if self.display_actions: |
|
208 if self.add_view_actions: |
|
209 actions = self.view.table_actions() |
|
210 if self.display_filter and self.hide_filter and (facetsform or not generate_form): |
|
211 actions += self.show_hide_filter_actions(not generate_form) |
|
212 self.render_table(w, actions, self.view.paginable) |
|
213 if facetsform and self.display_filter == 'bottom': |
|
214 cssclass = u'hidden' if self.hide_filter else u'' |
|
215 facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, |
|
216 divid=self.view.domid) |
|
217 |
|
218 def render_table_headers(self, w, colrenderers): |
|
219 w(u'<thead><tr>') |
|
220 for colrenderer in colrenderers: |
|
221 if colrenderer.sortable: |
|
222 w(u'<th class="sortable">') |
|
223 else: |
|
224 w(u'<th>') |
|
225 colrenderer.render_header(w) |
|
226 w(u'</th>') |
|
227 w(u'</tr></thead>\n') |
|
228 |
|
229 def render_table_body(self, w, colrenderers): |
|
230 w(u'<tbody>') |
|
231 for rownum in range(self.view.table_size): |
|
232 self.render_row(w, rownum, colrenderers) |
|
233 w(u'</tbody>') |
|
234 |
|
235 def render_table(self, w, actions, paginate): |
|
236 view = self.view |
|
237 divid = view.domid |
|
238 if divid is not None: |
|
239 w(u'<div id="%s">' % divid) |
|
240 else: |
|
241 assert not (actions or paginate) |
|
242 nav_html = UStringIO() |
|
243 if paginate: |
|
244 view.paginate(w=nav_html.write, show_all_option=self.show_all_option) |
|
245 w(nav_html.getvalue()) |
|
246 if actions and self.display_actions == 'top': |
|
247 self.render_actions(w, actions) |
|
248 colrenderers = view.build_column_renderers() |
|
249 attrs = self.table_attributes() |
|
250 w(u'<table %s>' % sgml_attributes(attrs)) |
|
251 if self.view.has_headers: |
|
252 self.render_table_headers(w, colrenderers) |
|
253 self.render_table_body(w, colrenderers) |
|
254 w(u'</table>') |
|
255 if actions and self.display_actions == 'bottom': |
|
256 self.render_actions(w, actions) |
|
257 w(nav_html.getvalue()) |
|
258 if divid is not None: |
|
259 w(u'</div>') |
|
260 |
|
261 def table_attributes(self): |
|
262 return {'class': self.cssclass} |
|
263 |
|
264 def render_row(self, w, rownum, renderers): |
|
265 attrs = self.row_attributes(rownum) |
|
266 w(u'<tr %s>' % sgml_attributes(attrs)) |
|
267 for colnum, renderer in enumerate(renderers): |
|
268 self.render_cell(w, rownum, colnum, renderer) |
|
269 w(u'</tr>\n') |
|
270 |
|
271 def row_attributes(self, rownum): |
|
272 return {'class': 'odd' if (rownum%2==1) else 'even', |
|
273 'onmouseover': '$(this).addClass("highlighted");', |
|
274 'onmouseout': '$(this).removeClass("highlighted")'} |
|
275 |
|
276 def render_cell(self, w, rownum, colnum, renderer): |
|
277 attrs = self.cell_attributes(rownum, colnum, renderer) |
|
278 if colnum in self.header_column_idx: |
|
279 tag = u'th' |
|
280 else: |
|
281 tag = u'td' |
|
282 w(u'<%s %s>' % (tag, sgml_attributes(attrs))) |
|
283 renderer.render_cell(w, rownum) |
|
284 w(u'</%s>' % tag) |
|
285 |
|
286 def cell_attributes(self, rownum, _colnum, renderer): |
|
287 attrs = renderer.attributes.copy() |
|
288 if renderer.sortable: |
|
289 sortvalue = renderer.sortvalue(rownum) |
|
290 if isinstance(sortvalue, string_types): |
|
291 sortvalue = sortvalue[:self.sortvalue_limit] |
|
292 if sortvalue is not None: |
|
293 attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue) |
|
294 return attrs |
|
295 |
|
296 def render_actions(self, w, actions): |
|
297 box = MenuWidget('', '', _class='tableActionsBox', islist=False) |
|
298 label = tags.span(self._cw._('action menu')) |
|
299 menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', |
|
300 ident='%sActions' % self.view.domid) |
|
301 box.append(menu) |
|
302 for action in actions: |
|
303 menu.append(action) |
|
304 box.render(w=w) |
|
305 w(u'<div class="clear"></div>') |
|
306 |
|
307 def show_hide_filter_actions(self, currentlydisplayed=False): |
|
308 divid = self.view.domid |
|
309 showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] |
|
310 for what in ('Form', 'Show', 'Hide', 'Actions')) |
|
311 showhide = 'javascript:' + showhide |
|
312 self._cw.add_onload(u'''\ |
|
313 $(document).ready(function() { |
|
314 if ($('#%(id)sForm[class=\"hidden\"]').length) { |
|
315 $('#%(id)sHide').attr('class', 'hidden'); |
|
316 } else { |
|
317 $('#%(id)sShow').attr('class', 'hidden'); |
|
318 } |
|
319 });''' % {'id': divid}) |
|
320 showlabel = self._cw._('show filter form') |
|
321 hidelabel = self._cw._('hide filter form') |
|
322 return [component.Link(showhide, showlabel, id='%sShow' % divid), |
|
323 component.Link(showhide, hidelabel, id='%sHide' % divid)] |
|
324 |
|
325 |
|
326 class AbstractColumnRenderer(object): |
|
327 """Abstract base class for column renderer. Interface of a column renderer follows: |
|
328 |
|
329 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.bind |
|
330 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_header |
|
331 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_cell |
|
332 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.sortvalue |
|
333 |
|
334 Attributes on this base class are: |
|
335 |
|
336 :attr: `header`, the column header. If None, default to `_(colid)` |
|
337 :attr: `addcount`, if True, add the table size in parenthezis beside the header |
|
338 :attr: `trheader`, should the header be translated |
|
339 :attr: `escapeheader`, should the header be xml_escaped |
|
340 :attr: `sortable`, tell if the column is sortable |
|
341 :attr: `view`, the table view |
|
342 :attr: `_cw`, the request object |
|
343 :attr: `colid`, the column identifier |
|
344 :attr: `attributes`, dictionary of attributes to put on the HTML tag when |
|
345 the cell is rendered |
|
346 """ #'# make emacs |
|
347 attributes = {} |
|
348 empty_cell_content = u' ' |
|
349 |
|
350 def __init__(self, header=None, addcount=False, trheader=True, |
|
351 escapeheader=True, sortable=True): |
|
352 self.header = header |
|
353 self.trheader = trheader |
|
354 self.escapeheader = escapeheader |
|
355 self.addcount = addcount |
|
356 self.sortable = sortable |
|
357 self.view = None |
|
358 self._cw = None |
|
359 self.colid = None |
|
360 |
|
361 def __str__(self): |
|
362 return '<%s.%s (column %s) at 0x%x>' % (self.view.__class__.__name__, |
|
363 self.__class__.__name__, |
|
364 self.colid, id(self)) |
|
365 |
|
366 def bind(self, view, colid): |
|
367 """Bind the column renderer to its view. This is where `_cw`, `view`, |
|
368 `colid` are set and the method to override if you want to add more |
|
369 view/request depending attributes on your column render. |
|
370 """ |
|
371 self.view = view |
|
372 self._cw = view._cw |
|
373 self.colid = colid |
|
374 |
|
375 def copy(self): |
|
376 assert self.view is None |
|
377 return copy(self) |
|
378 |
|
379 def default_header(self): |
|
380 """Return header for this column if one has not been specified.""" |
|
381 return self._cw._(self.colid) |
|
382 |
|
383 def render_header(self, w): |
|
384 """Write label for the specified column by calling w().""" |
|
385 header = self.header |
|
386 if header is None: |
|
387 header = self.default_header() |
|
388 elif self.trheader and header: |
|
389 header = self._cw._(header) |
|
390 if self.addcount: |
|
391 header = '%s (%s)' % (header, self.view.table_size) |
|
392 if header: |
|
393 if self.escapeheader: |
|
394 header = xml_escape(header) |
|
395 else: |
|
396 header = self.empty_cell_content |
|
397 if self.sortable: |
|
398 header = tags.span( |
|
399 header, escapecontent=False, |
|
400 title=self._cw._('Click to sort on this column')) |
|
401 w(header) |
|
402 |
|
403 def render_cell(self, w, rownum): |
|
404 """Write value for the specified cell by calling w(). |
|
405 |
|
406 :param `rownum`: the row number in the table |
|
407 """ |
|
408 raise NotImplementedError() |
|
409 |
|
410 def sortvalue(self, _rownum): |
|
411 """Return typed value to be used for sorting on the specified column. |
|
412 |
|
413 :param `rownum`: the row number in the table |
|
414 """ |
|
415 return None |
|
416 |
|
417 |
|
418 class TableMixIn(component.LayoutableMixIn): |
|
419 """Abstract mix-in class for layout based tables. |
|
420 |
|
421 This default implementation's call method simply delegate to |
|
422 meth:`layout_render` that will select the renderer whose identifier is given |
|
423 by the :attr:`layout_id` attribute. |
|
424 |
|
425 Then it provides some default implementation for various parts of the API |
|
426 used by that layout. |
|
427 |
|
428 Abstract method you will have to override is: |
|
429 |
|
430 .. automethod:: build_column_renderers |
|
431 |
|
432 You may also want to overridde: |
|
433 |
|
434 .. autoattribute:: cubicweb.web.views.tableview.TableMixIn.table_size |
|
435 |
|
436 The :attr:`has_headers` boolean attribute tells if the table has some |
|
437 headers to be displayed. Default to `True`. |
|
438 """ |
|
439 __abstract__ = True |
|
440 # table layout to use |
|
441 layout_id = 'table_layout' |
|
442 # true if the table has some headers |
|
443 has_headers = True |
|
444 # dictionary {colid : column renderer} |
|
445 column_renderers = {} |
|
446 # default renderer class to use when no renderer specified for the column |
|
447 default_column_renderer_class = None |
|
448 # default layout handles inner pagination |
|
449 handle_pagination = True |
|
450 |
|
451 def call(self, **kwargs): |
|
452 self._cw.add_js('cubicweb.ajax.js') # for pagination |
|
453 self.layout_render(self.w) |
|
454 |
|
455 def column_renderer(self, colid, *args, **kwargs): |
|
456 """Return a column renderer for column of the given id.""" |
|
457 try: |
|
458 crenderer = self.column_renderers[colid].copy() |
|
459 except KeyError: |
|
460 crenderer = self.default_column_renderer_class(*args, **kwargs) |
|
461 crenderer.bind(self, colid) |
|
462 return crenderer |
|
463 |
|
464 # layout callbacks ######################################################### |
|
465 |
|
466 def facets_form(self, **kwargs):# XXX extracted from jqplot cube |
|
467 return self._cw.vreg['views'].select_or_none( |
|
468 'facet.filtertable', self._cw, rset=self.cw_rset, view=self, |
|
469 **kwargs) |
|
470 |
|
471 @cachedproperty |
|
472 def domid(self): |
|
473 return self._cw.form.get('divid') or domid('%s-%s' % (self.__regid__, make_uid())) |
|
474 |
|
475 @property |
|
476 def table_size(self): |
|
477 """Return the number of rows (header excluded) to be displayed. |
|
478 |
|
479 By default return the number of rows in the view's result set. If your |
|
480 table isn't reult set based, override this method. |
|
481 """ |
|
482 return self.cw_rset.rowcount |
|
483 |
|
484 def build_column_renderers(self): |
|
485 """Return a list of column renderers, one for each column to be |
|
486 rendered. Prototype of a column renderer is described below: |
|
487 |
|
488 .. autoclass:: cubicweb.web.views.tableview.AbstractColumnRenderer |
|
489 """ |
|
490 raise NotImplementedError() |
|
491 |
|
492 def table_actions(self): |
|
493 """Return a list of actions (:class:`~cubicweb.web.component.Link`) that |
|
494 match the view's result set, and return those in the 'mainactions' |
|
495 category. |
|
496 """ |
|
497 req = self._cw |
|
498 actions = [] |
|
499 actionsbycat = req.vreg['actions'].possible_actions(req, self.cw_rset) |
|
500 for action in actionsbycat.get('mainactions', ()): |
|
501 for action in action.actual_actions(): |
|
502 actions.append(component.Link(action.url(), req._(action.title), |
|
503 klass=action.html_class()) ) |
|
504 return actions |
|
505 |
|
506 # interaction with navigation component #################################### |
|
507 |
|
508 def page_navigation_url(self, navcomp, _path, params): |
|
509 params['divid'] = self.domid |
|
510 params['vid'] = self.__regid__ |
|
511 return navcomp.ajax_page_url(**params) |
|
512 |
|
513 |
|
514 class RsetTableColRenderer(AbstractColumnRenderer): |
|
515 """Default renderer for :class:`RsetTableView`.""" |
|
516 |
|
517 def __init__(self, cellvid, **kwargs): |
|
518 super(RsetTableColRenderer, self).__init__(**kwargs) |
|
519 self.cellvid = cellvid |
|
520 |
|
521 def bind(self, view, colid): |
|
522 super(RsetTableColRenderer, self).bind(view, colid) |
|
523 self.cw_rset = view.cw_rset |
|
524 def render_cell(self, w, rownum): |
|
525 self._cw.view(self.cellvid, self.cw_rset, 'empty-cell', |
|
526 row=rownum, col=self.colid, w=w) |
|
527 |
|
528 # limit value's length as much as possible (e.g. by returning the 10 first |
|
529 # characters of a string) |
|
530 def sortvalue(self, rownum): |
|
531 colid = self.colid |
|
532 val = self.cw_rset[rownum][colid] |
|
533 if val is None: |
|
534 return u'' |
|
535 etype = self.cw_rset.description[rownum][colid] |
|
536 if etype is None: |
|
537 return u'' |
|
538 if self._cw.vreg.schema.eschema(etype).final: |
|
539 entity, rtype = self.cw_rset.related_entity(rownum, colid) |
|
540 if entity is None: |
|
541 return val # remove_html_tags() ? |
|
542 return entity.sortvalue(rtype) |
|
543 entity = self.cw_rset.get_entity(rownum, colid) |
|
544 return entity.sortvalue() |
|
545 |
|
546 |
|
547 class RsetTableView(TableMixIn, AnyRsetView): |
|
548 """This table view accepts any non-empty rset. It uses introspection on the |
|
549 result set to compute column names and the proper way to display the cells. |
|
550 |
|
551 It is highly configurable and accepts a wealth of options, but take care to |
|
552 check what you're trying to achieve wouldn't be a job for the |
|
553 :class:`EntityTableView`. Basically the question is: does this view should |
|
554 be tied to the result set query's shape or no? If yes, than you're fine. If |
|
555 no, you should take a look at the other table implementation. |
|
556 |
|
557 The following class attributes may be used to control the table: |
|
558 |
|
559 * `finalvid`, a view identifier that should be called on final entities |
|
560 (e.g. attribute values). Default to 'final'. |
|
561 |
|
562 * `nonfinalvid`, a view identifier that should be called on |
|
563 entities. Default to 'incontext'. |
|
564 |
|
565 * `displaycols`, if not `None`, should be a list of rset's columns to be |
|
566 displayed. |
|
567 |
|
568 * `headers`, if not `None`, should be a list of headers for the table's |
|
569 columns. `None` values in the list will be replaced by computed column |
|
570 names. |
|
571 |
|
572 * `cellvids`, if not `None`, should be a dictionary with table column index |
|
573 as key and a view identifier as value, telling the view that should be |
|
574 used in the given column. |
|
575 |
|
576 Notice `displaycols`, `headers` and `cellvids` may be specified at selection |
|
577 time but then the table won't have pagination and shouldn't be configured to |
|
578 display the facets filter nor actions (as they wouldn't behave as expected). |
|
579 |
|
580 This table class use the :class:`RsetTableColRenderer` as default column |
|
581 renderer. |
|
582 |
|
583 .. autoclass:: RsetTableColRenderer |
|
584 """ #'# make emacs happier |
|
585 __regid__ = 'table' |
|
586 # selector trick for bw compath with the former :class:TableView |
|
587 __select__ = AnyRsetView.__select__ & (~match_kwargs( |
|
588 'title', 'subvid', 'displayfilter', 'headers', 'displaycols', |
|
589 'displayactions', 'actions', 'divid', 'cellvids', 'cellattrs', |
|
590 'mainindex', 'paginate', 'page_size', mode='any') |
|
591 | unreloadable_table()) |
|
592 title = _('table') |
|
593 # additional configuration parameters |
|
594 finalvid = 'final' |
|
595 nonfinalvid = 'incontext' |
|
596 displaycols = None |
|
597 headers = None |
|
598 cellvids = None |
|
599 default_column_renderer_class = RsetTableColRenderer |
|
600 |
|
601 def linkable(self): |
|
602 # specific subclasses of this view usually don't want to be linkable |
|
603 # since they depends on a particular shape (being linkable meaning view |
|
604 # may be listed in possible views |
|
605 return self.__regid__ == 'table' |
|
606 |
|
607 def call(self, headers=None, displaycols=None, cellvids=None, |
|
608 paginate=None, **kwargs): |
|
609 if self.headers: |
|
610 self.headers = [h and self._cw._(h) for h in self.headers] |
|
611 if (headers or displaycols or cellvids or paginate): |
|
612 if headers is not None: |
|
613 self.headers = headers |
|
614 if displaycols is not None: |
|
615 self.displaycols = displaycols |
|
616 if cellvids is not None: |
|
617 self.cellvids = cellvids |
|
618 if paginate is not None: |
|
619 self.paginable = paginate |
|
620 if kwargs: |
|
621 # old table view arguments that we can safely ignore thanks to |
|
622 # selectors |
|
623 if len(kwargs) > 1: |
|
624 msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs) |
|
625 else: |
|
626 msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs) |
|
627 warn(msg, DeprecationWarning, stacklevel=2) |
|
628 super(RsetTableView, self).call(**kwargs) |
|
629 |
|
630 def main_var_index(self): |
|
631 """returns the index of the first non-attribute variable among the RQL |
|
632 selected variables |
|
633 """ |
|
634 eschema = self._cw.vreg.schema.eschema |
|
635 for i, etype in enumerate(self.cw_rset.description[0]): |
|
636 if not eschema(etype).final: |
|
637 return i |
|
638 return None |
|
639 |
|
640 # layout callbacks ######################################################### |
|
641 |
|
642 @property |
|
643 def table_size(self): |
|
644 """return the number of rows (header excluded) to be displayed""" |
|
645 return self.cw_rset.rowcount |
|
646 |
|
647 def build_column_renderers(self): |
|
648 headers = self.headers |
|
649 # compute displayed columns |
|
650 if self.displaycols is None: |
|
651 if headers is not None: |
|
652 displaycols = list(range(len(headers))) |
|
653 else: |
|
654 rqlst = self.cw_rset.syntax_tree() |
|
655 displaycols = list(range(len(rqlst.children[0].selection))) |
|
656 else: |
|
657 displaycols = self.displaycols |
|
658 # compute table headers |
|
659 main_var_index = self.main_var_index() |
|
660 computed_titles = self.columns_labels(main_var_index) |
|
661 # compute build renderers |
|
662 cellvids = self.cellvids |
|
663 renderers = [] |
|
664 for colnum, colid in enumerate(displaycols): |
|
665 addcount = False |
|
666 # compute column header |
|
667 title = None |
|
668 if headers is not None: |
|
669 title = headers[colnum] |
|
670 if title is None: |
|
671 title = computed_titles[colid] |
|
672 if colid == main_var_index: |
|
673 addcount = True |
|
674 # compute cell vid for the column |
|
675 if cellvids is not None and colnum in cellvids: |
|
676 cellvid = cellvids[colnum] |
|
677 else: |
|
678 coltype = self.cw_rset.description[0][colid] |
|
679 if coltype is not None and self._cw.vreg.schema.eschema(coltype).final: |
|
680 cellvid = self.finalvid |
|
681 else: |
|
682 cellvid = self.nonfinalvid |
|
683 # get renderer |
|
684 renderer = self.column_renderer(colid, header=title, trheader=False, |
|
685 addcount=addcount, cellvid=cellvid) |
|
686 renderers.append(renderer) |
|
687 return renderers |
|
688 |
|
689 |
|
690 class EntityTableColRenderer(AbstractColumnRenderer): |
|
691 """Default column renderer for :class:`EntityTableView`. |
|
692 |
|
693 You may use the :meth:`entity` method to retrieve the main entity for a |
|
694 given row number. |
|
695 |
|
696 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity |
|
697 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.render_entity |
|
698 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity_sortvalue |
|
699 """ |
|
700 def __init__(self, renderfunc=None, sortfunc=None, sortable=None, **kwargs): |
|
701 if renderfunc is None: |
|
702 renderfunc = self.render_entity |
|
703 # if renderfunc nor sortfunc nor sortable specified, column will be |
|
704 # sortable using the default implementation. |
|
705 if sortable is None: |
|
706 sortable = True |
|
707 # no sortfunc given but asked to be sortable: use the default sort |
|
708 # method. Sub-class may set `entity_sortvalue` to None if they don't |
|
709 # support sorting. |
|
710 if sortfunc is None and sortable: |
|
711 sortfunc = self.entity_sortvalue |
|
712 # at this point `sortable` may still be unspecified while `sortfunc` is |
|
713 # sure to be set to someting else than None if the column is sortable. |
|
714 sortable = sortfunc is not None |
|
715 super(EntityTableColRenderer, self).__init__(sortable=sortable, **kwargs) |
|
716 self.renderfunc = renderfunc |
|
717 self.sortfunc = sortfunc |
|
718 |
|
719 def copy(self): |
|
720 assert self.view is None |
|
721 # copy of attribute referencing a method doesn't work with python < 2.7 |
|
722 renderfunc = self.__dict__.pop('renderfunc') |
|
723 sortfunc = self.__dict__.pop('sortfunc') |
|
724 try: |
|
725 acopy = copy(self) |
|
726 for aname, member in[('renderfunc', renderfunc), |
|
727 ('sortfunc', sortfunc)]: |
|
728 if isinstance(member, MethodType): |
|
729 member = create_bound_method(member.__func__, acopy) |
|
730 setattr(acopy, aname, member) |
|
731 return acopy |
|
732 finally: |
|
733 self.renderfunc = renderfunc |
|
734 self.sortfunc = sortfunc |
|
735 |
|
736 def render_cell(self, w, rownum): |
|
737 entity = self.entity(rownum) |
|
738 if entity is None: |
|
739 w(self.empty_cell_content) |
|
740 else: |
|
741 self.renderfunc(w, entity) |
|
742 |
|
743 def sortvalue(self, rownum): |
|
744 entity = self.entity(rownum) |
|
745 if entity is None: |
|
746 return None |
|
747 else: |
|
748 return self.sortfunc(entity) |
|
749 |
|
750 def entity(self, rownum): |
|
751 """Convenience method returning the table's main entity.""" |
|
752 return self.view.entity(rownum) |
|
753 |
|
754 def render_entity(self, w, entity): |
|
755 """Sort value if `renderfunc` nor `sortfunc` specified at |
|
756 initialization. |
|
757 |
|
758 This default implementation consider column id is an entity attribute |
|
759 and print its value. |
|
760 """ |
|
761 w(entity.printable_value(self.colid)) |
|
762 |
|
763 def entity_sortvalue(self, entity): |
|
764 """Cell rendering implementation if `renderfunc` nor `sortfunc` |
|
765 specified at initialization. |
|
766 |
|
767 This default implementation consider column id is an entity attribute |
|
768 and return its sort value by calling `entity.sortvalue(colid)`. |
|
769 """ |
|
770 return entity.sortvalue(self.colid) |
|
771 |
|
772 |
|
773 class MainEntityColRenderer(EntityTableColRenderer): |
|
774 """Renderer to be used for the column displaying the 'main entity' of a |
|
775 :class:`EntityTableView`. |
|
776 |
|
777 By default display it using the 'incontext' view. You may specify another |
|
778 view identifier using the `vid` argument. |
|
779 |
|
780 If header not specified, it would be built using entity types in the main |
|
781 column. |
|
782 """ |
|
783 def __init__(self, vid='incontext', addcount=True, **kwargs): |
|
784 super(MainEntityColRenderer, self).__init__(addcount=addcount, **kwargs) |
|
785 self.vid = vid |
|
786 |
|
787 def default_header(self): |
|
788 view = self.view |
|
789 if len(view.cw_rset) > 1: |
|
790 suffix = '_plural' |
|
791 else: |
|
792 suffix = '' |
|
793 return u', '.join(self._cw.__(et + suffix) |
|
794 for et in view.cw_rset.column_types(view.cw_col or 0)) |
|
795 |
|
796 def render_entity(self, w, entity): |
|
797 entity.view(self.vid, w=w) |
|
798 |
|
799 def entity_sortvalue(self, entity): |
|
800 return entity.sortvalue() |
|
801 |
|
802 |
|
803 class RelatedEntityColRenderer(MainEntityColRenderer): |
|
804 """Renderer to be used for column displaying an entity related the 'main |
|
805 entity' of a :class:`EntityTableView`. |
|
806 |
|
807 By default display it using the 'incontext' view. You may specify another |
|
808 view identifier using the `vid` argument. |
|
809 |
|
810 If header not specified, it would be built by translating the column id. |
|
811 """ |
|
812 def __init__(self, getrelated, addcount=False, **kwargs): |
|
813 super(RelatedEntityColRenderer, self).__init__(addcount=addcount, **kwargs) |
|
814 self.getrelated = getrelated |
|
815 |
|
816 def entity(self, rownum): |
|
817 entity = super(RelatedEntityColRenderer, self).entity(rownum) |
|
818 return self.getrelated(entity) |
|
819 |
|
820 def default_header(self): |
|
821 return self._cw._(self.colid) |
|
822 |
|
823 |
|
824 class RelationColRenderer(EntityTableColRenderer): |
|
825 """Renderer to be used for column displaying a list of entities related the |
|
826 'main entity' of a :class:`EntityTableView`. By default, the main entity is |
|
827 considered as the subject of the relation but you may specify otherwise |
|
828 using the `role` argument. |
|
829 |
|
830 By default display the related rset using the 'csv' view, using |
|
831 'outofcontext' sub-view for each entity. You may specify another view |
|
832 identifier using respectivly the `vid` and `subvid` arguments. |
|
833 |
|
834 If you specify a 'rtype view', such as 'reledit', you should add a |
|
835 is_rtype_view=True parameter. |
|
836 |
|
837 If header not specified, it would be built by translating the column id, |
|
838 properly considering role. |
|
839 """ |
|
840 def __init__(self, role='subject', vid='csv', subvid=None, |
|
841 fallbackvid='empty-cell', is_rtype_view=False, **kwargs): |
|
842 super(RelationColRenderer, self).__init__(**kwargs) |
|
843 self.role = role |
|
844 self.vid = vid |
|
845 if subvid is None and vid in ('csv', 'list'): |
|
846 subvid = 'outofcontext' |
|
847 self.subvid = subvid |
|
848 self.fallbackvid = fallbackvid |
|
849 self.is_rtype_view = is_rtype_view |
|
850 |
|
851 def render_entity(self, w, entity): |
|
852 kwargs = {'w': w} |
|
853 if self.is_rtype_view: |
|
854 rset = None |
|
855 kwargs['entity'] = entity |
|
856 kwargs['rtype'] = self.colid |
|
857 kwargs['role'] = self.role |
|
858 else: |
|
859 rset = entity.related(self.colid, self.role) |
|
860 if self.subvid is not None: |
|
861 kwargs['subvid'] = self.subvid |
|
862 self._cw.view(self.vid, rset, self.fallbackvid, **kwargs) |
|
863 |
|
864 def default_header(self): |
|
865 return display_name(self._cw, self.colid, self.role) |
|
866 |
|
867 entity_sortvalue = None # column not sortable by default |
|
868 |
|
869 |
|
870 class EntityTableView(TableMixIn, EntityView): |
|
871 """This abstract table view is designed to be used with an |
|
872 :class:`is_instance()` or :class:`adaptable` predicate, hence doesn't depend |
|
873 the result set shape as the :class:`RsetTableView` does. |
|
874 |
|
875 It will display columns that should be defined using the `columns` class |
|
876 attribute containing a list of column ids. By default, each column is |
|
877 renderered by :class:`EntityTableColRenderer` which consider that the column |
|
878 id is an attribute of the table's main entity (ie the one for which the view |
|
879 is selected). |
|
880 |
|
881 You may wish to specify :class:`MainEntityColRenderer` or |
|
882 :class:`RelatedEntityColRenderer` renderer for a column in the |
|
883 :attr:`column_renderers` dictionary. |
|
884 |
|
885 .. autoclass:: cubicweb.web.views.tableview.EntityTableColRenderer |
|
886 .. autoclass:: cubicweb.web.views.tableview.MainEntityColRenderer |
|
887 .. autoclass:: cubicweb.web.views.tableview.RelatedEntityColRenderer |
|
888 .. autoclass:: cubicweb.web.views.tableview.RelationColRenderer |
|
889 """ |
|
890 __abstract__ = True |
|
891 default_column_renderer_class = EntityTableColRenderer |
|
892 columns = None # to be defined in concret class |
|
893 |
|
894 def call(self, columns=None, **kwargs): |
|
895 if columns is not None: |
|
896 self.columns = columns |
|
897 self.layout_render(self.w) |
|
898 |
|
899 @property |
|
900 def table_size(self): |
|
901 return self.cw_rset.rowcount |
|
902 |
|
903 def build_column_renderers(self): |
|
904 return [self.column_renderer(colid) for colid in self.columns] |
|
905 |
|
906 def entity(self, rownum): |
|
907 """Return the table's main entity""" |
|
908 return self.cw_rset.get_entity(rownum, self.cw_col or 0) |
|
909 |
|
910 |
|
911 class EmptyCellView(AnyRsetView): |
|
912 __regid__ = 'empty-cell' |
|
913 __select__ = yes() |
|
914 def call(self, **kwargs): |
|
915 self.w(u' ') |
|
916 cell_call = call |
|
917 |
|
918 |
|
919 ################################################################################ |
|
920 # DEPRECATED tables ############################################################ |
|
921 ################################################################################ |
|
922 |
|
923 |
|
924 @add_metaclass(class_deprecated) |
|
925 class TableView(AnyRsetView): |
|
926 """The table view accepts any non-empty rset. It uses introspection on the |
|
927 result set to compute column names and the proper way to display the cells. |
|
928 |
|
929 It is however highly configurable and accepts a wealth of options. |
|
930 """ |
|
931 __deprecation_warning__ = '[3.14] %(cls)s is deprecated' |
|
932 __regid__ = 'table' |
|
933 title = _('table') |
|
934 finalview = 'final' |
|
935 |
|
936 table_widget_class = TableWidget |
|
937 table_column_class = TableColumn |
|
938 |
|
939 tablesorter_settings = { |
|
940 'textExtraction': JSString('cw.sortValueExtraction'), |
|
941 'selectorHeaders': 'thead tr:first th', # only plug on the first row |
|
942 } |
|
943 handle_pagination = True |
|
944 |
|
945 def form_filter(self, divid, displaycols, displayactions, displayfilter, |
|
946 paginate, hidden=True): |
|
947 try: |
|
948 filterform = self._cw.vreg['views'].select( |
|
949 'facet.filtertable', self._cw, rset=self.cw_rset) |
|
950 except NoSelectableObject: |
|
951 return () |
|
952 vidargs = {'paginate': paginate, |
|
953 'displaycols': displaycols, |
|
954 'displayactions': displayactions, |
|
955 'displayfilter': displayfilter} |
|
956 cssclass = hidden and 'hidden' or '' |
|
957 filterform.render(self.w, vid=self.__regid__, divid=divid, |
|
958 vidargs=vidargs, cssclass=cssclass) |
|
959 return self.show_hide_actions(divid, not hidden) |
|
960 |
|
961 def main_var_index(self): |
|
962 """Returns the index of the first non final variable of the rset. |
|
963 |
|
964 Used to select the main etype to help generate accurate column headers. |
|
965 XXX explain the concept |
|
966 |
|
967 May return None if none is found. |
|
968 """ |
|
969 eschema = self._cw.vreg.schema.eschema |
|
970 for i, etype in enumerate(self.cw_rset.description[0]): |
|
971 try: |
|
972 if not eschema(etype).final: |
|
973 return i |
|
974 except KeyError: # XXX possible? |
|
975 continue |
|
976 return None |
|
977 |
|
978 def displaycols(self, displaycols, headers): |
|
979 if displaycols is None: |
|
980 if 'displaycols' in self._cw.form: |
|
981 displaycols = [int(idx) for idx in self._cw.form['displaycols']] |
|
982 elif headers is not None: |
|
983 displaycols = list(range(len(headers))) |
|
984 else: |
|
985 displaycols = list(range(len(self.cw_rset.syntax_tree().children[0].selection))) |
|
986 return displaycols |
|
987 |
|
988 def _setup_tablesorter(self, divid): |
|
989 req = self._cw |
|
990 req.add_js('jquery.tablesorter.js') |
|
991 req.add_onload('''$(document).ready(function() { |
|
992 $("#%s table.listing").tablesorter(%s); |
|
993 });''' % (divid, js_dumps(self.tablesorter_settings))) |
|
994 req.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css')) |
|
995 |
|
996 @cachedproperty |
|
997 def initial_load(self): |
|
998 """We detect a bit heuristically if we are built for the first time or |
|
999 from subsequent calls by the form filter or by the pagination |
|
1000 hooks. |
|
1001 |
|
1002 """ |
|
1003 form = self._cw.form |
|
1004 return 'fromformfilter' not in form and '__start' not in form |
|
1005 |
|
1006 def call(self, title=None, subvid=None, displayfilter=None, headers=None, |
|
1007 displaycols=None, displayactions=None, actions=(), divid=None, |
|
1008 cellvids=None, cellattrs=None, mainindex=None, |
|
1009 paginate=False, page_size=None): |
|
1010 """Produces a table displaying a composite query |
|
1011 |
|
1012 :param title: title added before table |
|
1013 :param subvid: cell view |
|
1014 :param displayfilter: filter that selects rows to display |
|
1015 :param headers: columns' titles |
|
1016 :param displaycols: indexes of columns to display (first column is 0) |
|
1017 :param displayactions: if True, display action menu |
|
1018 """ |
|
1019 req = self._cw |
|
1020 divid = divid or req.form.get('divid') or 'rs%s' % make_uid(id(self.cw_rset)) |
|
1021 self._setup_tablesorter(divid) |
|
1022 # compute label first since the filter form may remove some necessary |
|
1023 # information from the rql syntax tree |
|
1024 if mainindex is None: |
|
1025 mainindex = self.main_var_index() |
|
1026 computed_labels = self.columns_labels(mainindex) |
|
1027 if not subvid and 'subvid' in req.form: |
|
1028 subvid = req.form.pop('subvid') |
|
1029 actions = list(actions) |
|
1030 if mainindex is None: |
|
1031 displayfilter, displayactions = False, False |
|
1032 else: |
|
1033 if displayfilter is None and req.form.get('displayfilter'): |
|
1034 displayfilter = True |
|
1035 if displayactions is None and req.form.get('displayactions'): |
|
1036 displayactions = True |
|
1037 displaycols = self.displaycols(displaycols, headers) |
|
1038 if self.initial_load: |
|
1039 self.w(u'<div class="section">') |
|
1040 if not title and 'title' in req.form: |
|
1041 title = req.form['title'] |
|
1042 if title: |
|
1043 self.w(u'<h2 class="tableTitle">%s</h2>\n' % title) |
|
1044 if displayfilter: |
|
1045 actions += self.form_filter(divid, displaycols, displayfilter, |
|
1046 displayactions, paginate) |
|
1047 elif displayfilter: |
|
1048 actions += self.show_hide_actions(divid, True) |
|
1049 self.w(u'<div id="%s">' % divid) |
|
1050 if displayactions: |
|
1051 actionsbycat = self._cw.vreg['actions'].possible_actions(req, self.cw_rset) |
|
1052 for action in actionsbycat.get('mainactions', ()): |
|
1053 for action in action.actual_actions(): |
|
1054 actions.append( (action.url(), req._(action.title), |
|
1055 action.html_class(), None) ) |
|
1056 # render actions menu |
|
1057 if actions: |
|
1058 self.render_actions(divid, actions) |
|
1059 # render table |
|
1060 if paginate: |
|
1061 self.divid = divid # XXX iirk (see usage in page_navigation_url) |
|
1062 self.paginate(page_size=page_size, show_all_option=False) |
|
1063 table = self.table_widget_class(self) |
|
1064 for column in self.get_columns(computed_labels, displaycols, headers, |
|
1065 subvid, cellvids, cellattrs, mainindex): |
|
1066 table.append_column(column) |
|
1067 table.render(self.w) |
|
1068 self.w(u'</div>\n') |
|
1069 if self.initial_load: |
|
1070 self.w(u'</div>\n') |
|
1071 |
|
1072 def page_navigation_url(self, navcomp, path, params): |
|
1073 """Build a URL to the current view using the <navcomp> attributes |
|
1074 |
|
1075 :param navcomp: a NavigationComponent to call a URL method on. |
|
1076 :param path: expected to be json here? |
|
1077 :param params: params to give to build_url method |
|
1078 |
|
1079 this is called by :class:`cubiweb.web.component.NavigationComponent` |
|
1080 """ |
|
1081 if hasattr(self, 'divid'): |
|
1082 # XXX this assert a single call |
|
1083 params['divid'] = self.divid |
|
1084 params['vid'] = self.__regid__ |
|
1085 return navcomp.ajax_page_url(**params) |
|
1086 |
|
1087 def show_hide_actions(self, divid, currentlydisplayed=False): |
|
1088 showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] |
|
1089 for what in ('Form', 'Show', 'Hide', 'Actions')) |
|
1090 showhide = 'javascript:' + showhide |
|
1091 showlabel = self._cw._('show filter form') |
|
1092 hidelabel = self._cw._('hide filter form') |
|
1093 if currentlydisplayed: |
|
1094 return [(showhide, showlabel, 'hidden', '%sShow' % divid), |
|
1095 (showhide, hidelabel, None, '%sHide' % divid)] |
|
1096 return [(showhide, showlabel, None, '%sShow' % divid), |
|
1097 (showhide, hidelabel, 'hidden', '%sHide' % divid)] |
|
1098 |
|
1099 def render_actions(self, divid, actions): |
|
1100 box = MenuWidget('', 'tableActionsBox', _class='', islist=False) |
|
1101 label = tags.img(src=self._cw.uiprops['PUCE_DOWN'], |
|
1102 alt=xml_escape(self._cw._('action(s) on this selection'))) |
|
1103 menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', |
|
1104 ident='%sActions' % divid) |
|
1105 box.append(menu) |
|
1106 for url, label, klass, ident in actions: |
|
1107 menu.append(component.Link(url, label, klass=klass, id=ident)) |
|
1108 box.render(w=self.w) |
|
1109 self.w(u'<div class="clear"></div>') |
|
1110 |
|
1111 def get_columns(self, computed_labels, displaycols, headers, subvid, |
|
1112 cellvids, cellattrs, mainindex): |
|
1113 """build columns description from various parameters |
|
1114 |
|
1115 : computed_labels: columns headers computed from rset to be used if there is no headers entry |
|
1116 : displaycols: see :meth:`call` |
|
1117 : headers: explicitly define columns headers |
|
1118 : subvid: see :meth:`call` |
|
1119 : cellvids: see :meth:`call` |
|
1120 : cellattrs: see :meth:`call` |
|
1121 : mainindex: see :meth:`call` |
|
1122 |
|
1123 return a list of columns description to be used by |
|
1124 :class:`~cubicweb.web.htmlwidgets.TableWidget` |
|
1125 """ |
|
1126 columns = [] |
|
1127 eschema = self._cw.vreg.schema.eschema |
|
1128 for colindex, label in enumerate(computed_labels): |
|
1129 if colindex not in displaycols: |
|
1130 continue |
|
1131 # compute column header |
|
1132 if headers is not None: |
|
1133 _label = headers[displaycols.index(colindex)] |
|
1134 if _label is not None: |
|
1135 label = _label |
|
1136 if colindex == mainindex and label is not None: |
|
1137 label += ' (%s)' % self.cw_rset.rowcount |
|
1138 column = self.table_column_class(label, colindex) |
|
1139 coltype = self.cw_rset.description[0][colindex] |
|
1140 # compute column cell view (if coltype is None, it's a left outer |
|
1141 # join, use the default non final subvid) |
|
1142 if cellvids and colindex in cellvids: |
|
1143 column.append_renderer(cellvids[colindex], colindex) |
|
1144 elif coltype is not None and eschema(coltype).final: |
|
1145 column.append_renderer(self.finalview, colindex) |
|
1146 else: |
|
1147 column.append_renderer(subvid or 'incontext', colindex) |
|
1148 if cellattrs and colindex in cellattrs: |
|
1149 for name, value in cellattrs[colindex].items(): |
|
1150 column.add_attr(name, value) |
|
1151 # add column |
|
1152 columns.append(column) |
|
1153 return columns |
|
1154 |
|
1155 |
|
1156 def render_cell(self, cellvid, row, col, w): |
|
1157 self._cw.view('cell', self.cw_rset, row=row, col=col, cellvid=cellvid, w=w) |
|
1158 |
|
1159 def get_rows(self): |
|
1160 return self.cw_rset |
|
1161 |
|
1162 @htmlescape |
|
1163 @jsonize |
|
1164 @limitsize(10) |
|
1165 def sortvalue(self, row, col): |
|
1166 # XXX it might be interesting to try to limit value's |
|
1167 # length as much as possible (e.g. by returning the 10 |
|
1168 # first characters of a string) |
|
1169 val = self.cw_rset[row][col] |
|
1170 if val is None: |
|
1171 return u'' |
|
1172 etype = self.cw_rset.description[row][col] |
|
1173 if etype is None: |
|
1174 return u'' |
|
1175 if self._cw.vreg.schema.eschema(etype).final: |
|
1176 entity, rtype = self.cw_rset.related_entity(row, col) |
|
1177 if entity is None: |
|
1178 return val # remove_html_tags() ? |
|
1179 return entity.sortvalue(rtype) |
|
1180 entity = self.cw_rset.get_entity(row, col) |
|
1181 return entity.sortvalue() |
|
1182 |
|
1183 |
|
1184 class EditableTableView(TableView): |
|
1185 __regid__ = 'editable-table' |
|
1186 finalview = 'editable-final' |
|
1187 title = _('editable-table') |
|
1188 |
|
1189 |
|
1190 @add_metaclass(class_deprecated) |
|
1191 class CellView(EntityView): |
|
1192 __deprecation_warning__ = '[3.14] %(cls)s is deprecated' |
|
1193 __regid__ = 'cell' |
|
1194 __select__ = nonempty_rset() |
|
1195 |
|
1196 def cell_call(self, row, col, cellvid=None): |
|
1197 """ |
|
1198 :param row, col: indexes locating the cell value in view's result set |
|
1199 :param cellvid: cell view (defaults to 'outofcontext') |
|
1200 """ |
|
1201 etype, val = self.cw_rset.description[row][col], self.cw_rset[row][col] |
|
1202 if etype is None or not self._cw.vreg.schema.eschema(etype).final: |
|
1203 if val is None: |
|
1204 # This is usually caused by a left outer join and in that case, |
|
1205 # regular views will most certainly fail if they don't have |
|
1206 # a real eid |
|
1207 # XXX if cellvid is e.g. reledit, we may wanna call it anyway |
|
1208 self.w(u' ') |
|
1209 else: |
|
1210 self.wview(cellvid or 'outofcontext', self.cw_rset, row=row, col=col) |
|
1211 else: |
|
1212 # XXX why do we need a fallback view here? |
|
1213 self.wview(cellvid or 'final', self.cw_rset, 'null', row=row, col=col) |
|
1214 |
|
1215 |
|
1216 class InitialTableView(TableView): |
|
1217 """same display as table view but consider two rql queries : |
|
1218 |
|
1219 * the default query (ie `rql` form parameter), which is only used to select |
|
1220 this view and to build the filter form. This query should have the same |
|
1221 structure as the actual without actual restriction (but link to |
|
1222 restriction variables) and usually with a limit for efficiency (limit set |
|
1223 to 2 is advised) |
|
1224 |
|
1225 * the actual query (`actualrql` form parameter) whose results will be |
|
1226 displayed with default restrictions set |
|
1227 """ |
|
1228 __regid__ = 'initialtable' |
|
1229 __select__ = nonempty_rset() |
|
1230 # should not be displayed in possible view since it expects some specific |
|
1231 # parameters |
|
1232 title = None |
|
1233 |
|
1234 def call(self, title=None, subvid=None, headers=None, divid=None, |
|
1235 paginate=False, displaycols=None, displayactions=None, |
|
1236 mainindex=None): |
|
1237 """Dumps a table displaying a composite query""" |
|
1238 try: |
|
1239 actrql = self._cw.form['actualrql'] |
|
1240 except KeyError: |
|
1241 actrql = self.cw_rset.printable_rql() |
|
1242 else: |
|
1243 self._cw.ensure_ro_rql(actrql) |
|
1244 displaycols = self.displaycols(displaycols, headers) |
|
1245 if displayactions is None and 'displayactions' in self._cw.form: |
|
1246 displayactions = True |
|
1247 if divid is None and 'divid' in self._cw.form: |
|
1248 divid = self._cw.form['divid'] |
|
1249 self.w(u'<div class="section">') |
|
1250 if not title and 'title' in self._cw.form: |
|
1251 # pop title so it's not displayed by the table view as well |
|
1252 title = self._cw.form.pop('title') |
|
1253 if title: |
|
1254 self.w(u'<h2>%s</h2>\n' % title) |
|
1255 if mainindex is None: |
|
1256 mainindex = self.main_var_index() |
|
1257 if mainindex is not None: |
|
1258 actions = self.form_filter(divid, displaycols, displayactions, |
|
1259 displayfilter=True, paginate=paginate, |
|
1260 hidden=True) |
|
1261 else: |
|
1262 actions = () |
|
1263 if not subvid and 'subvid' in self._cw.form: |
|
1264 subvid = self._cw.form.pop('subvid') |
|
1265 self._cw.view('table', self._cw.execute(actrql), |
|
1266 'noresult', w=self.w, displayfilter=False, subvid=subvid, |
|
1267 displayactions=displayactions, displaycols=displaycols, |
|
1268 actions=actions, headers=headers, divid=divid) |
|
1269 self.w(u'</div>\n') |
|
1270 |
|
1271 |
|
1272 class EditableInitialTableTableView(InitialTableView): |
|
1273 __regid__ = 'editable-initialtable' |
|
1274 finalview = 'editable-final' |
|
1275 |
|
1276 |
|
1277 @add_metaclass(class_deprecated) |
|
1278 class EntityAttributesTableView(EntityView): |
|
1279 """This table displays entity attributes in a table and allow to set a |
|
1280 specific method to help building cell content for each attribute as well as |
|
1281 column header. |
|
1282 |
|
1283 Table will render entity cell by using the appropriate build_COLNAME_cell |
|
1284 methods if defined otherwise cell content will be entity.COLNAME. |
|
1285 |
|
1286 Table will render column header using the method header_for_COLNAME if |
|
1287 defined otherwise COLNAME will be used. |
|
1288 """ |
|
1289 __deprecation_warning__ = '[3.14] %(cls)s is deprecated' |
|
1290 __abstract__ = True |
|
1291 columns = () |
|
1292 table_css = "listing" |
|
1293 css_files = () |
|
1294 |
|
1295 def call(self, columns=None): |
|
1296 if self.css_files: |
|
1297 self._cw.add_css(self.css_files) |
|
1298 _ = self._cw._ |
|
1299 self.columns = columns or self.columns |
|
1300 sample = self.cw_rset.get_entity(0, 0) |
|
1301 self.w(u'<table class="%s">' % self.table_css) |
|
1302 self.table_header(sample) |
|
1303 self.w(u'<tbody>') |
|
1304 for row in range(self.cw_rset.rowcount): |
|
1305 self.cell_call(row=row, col=0) |
|
1306 self.w(u'</tbody>') |
|
1307 self.w(u'</table>') |
|
1308 |
|
1309 def cell_call(self, row, col): |
|
1310 _ = self._cw._ |
|
1311 entity = self.cw_rset.get_entity(row, col) |
|
1312 entity.complete() |
|
1313 infos = {} |
|
1314 for col in self.columns: |
|
1315 meth = getattr(self, 'build_%s_cell' % col, None) |
|
1316 # find the build method or try to find matching attribute |
|
1317 if meth: |
|
1318 content = meth(entity) |
|
1319 else: |
|
1320 content = entity.printable_value(col) |
|
1321 infos[col] = content |
|
1322 self.w(u"""<tr onmouseover="$(this).addClass('highlighted');" |
|
1323 onmouseout="$(this).removeClass('highlighted')">""") |
|
1324 line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns) |
|
1325 self.w(line % infos) |
|
1326 self.w(u'</tr>\n') |
|
1327 |
|
1328 def table_header(self, sample): |
|
1329 """builds the table's header""" |
|
1330 self.w(u'<thead><tr>') |
|
1331 for column in self.columns: |
|
1332 meth = getattr(self, 'header_for_%s' % column, None) |
|
1333 if meth: |
|
1334 colname = meth(sample) |
|
1335 else: |
|
1336 colname = self._cw._(column) |
|
1337 self.w(u'<th>%s</th>' % xml_escape(colname)) |
|
1338 self.w(u'</tr></thead>\n') |
|