13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. |
14 # details. |
15 # |
15 # |
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 table view, including filtering abilities using facets""" |
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 on 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 can still 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 implements you own class. |
|
58 |
|
59 .. autoclass:: cubicweb.web.views.tableview.TableMixIn |
|
60 :members: |
|
61 """ |
19 |
62 |
20 __docformat__ = "restructuredtext en" |
63 __docformat__ = "restructuredtext en" |
21 _ = unicode |
64 _ = unicode |
22 |
65 |
|
66 from warnings import warn |
|
67 from copy import copy |
|
68 from types import MethodType |
|
69 |
23 from logilab.mtconverter import xml_escape |
70 from logilab.mtconverter import xml_escape |
|
71 from logilab.common.decorators import cachedproperty |
|
72 from logilab.common.deprecation import class_deprecated |
24 |
73 |
25 from cubicweb import NoSelectableObject, tags |
74 from cubicweb import NoSelectableObject, tags |
26 from cubicweb.selectors import nonempty_rset |
75 from cubicweb.selectors import yes, nonempty_rset, match_kwargs, objectify_selector |
27 from cubicweb.utils import make_uid, js_dumps, JSString |
76 from cubicweb.schema import display_name |
|
77 from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO |
|
78 from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid |
28 from cubicweb.view import EntityView, AnyRsetView |
79 from cubicweb.view import EntityView, AnyRsetView |
29 from cubicweb.uilib import toggle_action, limitsize, htmlescape |
80 from cubicweb.web import jsonize, component |
30 from cubicweb.web import jsonize, component, facet |
|
31 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, |
81 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, |
32 PopupBoxMenu) |
82 PopupBoxMenu) |
|
83 |
|
84 |
|
85 @objectify_selector |
|
86 def unreloadable_table(cls, req, rset=None, |
|
87 displaycols=None, headers=None, cellvids=None, |
|
88 paginate=False, displayactions=False, displayfilter=False, |
|
89 **kwargs): |
|
90 # one may wish to specify one of headers/displaycols/cellvids as long as he |
|
91 # doesn't want pagination nor actions nor facets |
|
92 if not kwargs and (displaycols or headers or cellvids) and not ( |
|
93 displayfilter or displayactions or paginate): |
|
94 return 1 |
|
95 return 0 |
|
96 |
|
97 |
|
98 class TableLayout(component.Component): |
|
99 """The default layout for table. When `render` is called, this will use |
|
100 the API described on :class:`TableMixIn` to feed the generated table. |
|
101 |
|
102 This layout behaviour may be customized using the following attributes / |
|
103 selection arguments: |
|
104 |
|
105 * `cssclass`, a string that should be used as HTML class attribute. Default |
|
106 to "listing". |
|
107 |
|
108 * `needs_css`, the CSS files that should be used together with this |
|
109 table. Default to ('cubicweb.tablesorter.css', 'cubicweb.tableview.css'). |
|
110 |
|
111 * `needs_js`, the Javascript files that should be used together with this |
|
112 table. Default to ('jquery.tablesorter.js',) |
|
113 |
|
114 * `display_filter`, tells if the facets filter should be displayed when |
|
115 possible. Allowed values are: |
|
116 - `None`, don't display it |
|
117 - 'top', display it above the table |
|
118 - 'bottom', display it below the table |
|
119 |
|
120 * `display_actions`, tells if a menu for available actions should be |
|
121 displayed when possible (see two following options). Allowed values are: |
|
122 - `None`, don't display it |
|
123 - 'top', display it above the table |
|
124 - 'bottom', display it below the table |
|
125 |
|
126 * `hide_filter`, when true (the default), facets filter will be hidden by |
|
127 default, with an action in the actions menu allowing to show / hide it. |
|
128 |
|
129 * `show_all_option`, when true, a *show all results* link will be displayed |
|
130 below the navigation component. |
|
131 |
|
132 * `add_view_actions`, when true, actions returned by view.table_actions() |
|
133 will be included in the actions menu. |
|
134 |
|
135 * `header_column_idx`, if not `None`, should be a colum index or a set of |
|
136 column index where <th> tags should be generated instead of <td> |
|
137 """ #'# make emacs happier |
|
138 __regid__ = 'table_layout' |
|
139 cssclass = "listing" |
|
140 needs_css = ('cubicweb.tableview.css',) |
|
141 needs_js = () |
|
142 display_filter = None # None / 'top' / 'bottom' |
|
143 display_actions = 'top' # None / 'top' / 'bottom' |
|
144 hide_filter = True |
|
145 show_all_option = True # make navcomp generate a 'show all' results link |
|
146 add_view_actions = False |
|
147 header_column_idx = None |
|
148 enable_sorting = True |
|
149 sortvalue_limit = 10 |
|
150 tablesorter_settings = { |
|
151 'textExtraction': JSString('cw.sortValueExtraction'), |
|
152 'selectorHeaders': "thead tr:first th[class='sortable']", # only plug on the first row |
|
153 } |
|
154 |
|
155 def _setup_tablesorter(self, divid): |
|
156 self._cw.add_css('cubicweb.tablesorter.css') |
|
157 self._cw.add_js('jquery.tablesorter.js') |
|
158 self._cw.add_onload('''$(document).ready(function() { |
|
159 $("#%s table").tablesorter(%s); |
|
160 });''' % (divid, js_dumps(self.tablesorter_settings))) |
|
161 |
|
162 def __init__(self, req, view, **kwargs): |
|
163 super(TableLayout, self).__init__(req, **kwargs) |
|
164 for key, val in self.cw_extra_kwargs.items(): |
|
165 if hasattr(self.__class__, key) and not key[0] == '_': |
|
166 setattr(self, key, val) |
|
167 self.cw_extra_kwargs.pop(key) |
|
168 self.view = view |
|
169 if self.header_column_idx is None: |
|
170 self.header_column_idx = frozenset() |
|
171 elif isinstance(self.header_column_idx, int): |
|
172 self.header_column_idx = frozenset( (self.header_column_idx,) ) |
|
173 |
|
174 @cachedproperty |
|
175 def initial_load(self): |
|
176 """We detect a bit heuristically if we are built for the first time. |
|
177 or from subsequent calls by the form filter or by the pagination hooks. |
|
178 """ |
|
179 form = self._cw.form |
|
180 return 'fromformfilter' not in form and '__fromnavigation' not in form |
|
181 |
|
182 def render(self, w, **kwargs): |
|
183 assert self.display_filter in (None, 'top', 'bottom'), self.display_filter |
|
184 if self.needs_css: |
|
185 self._cw.add_css(self.needs_css) |
|
186 if self.needs_js: |
|
187 self._cw.add_js(self.needs_js) |
|
188 if self.enable_sorting: |
|
189 self._setup_tablesorter(self.view.domid) |
|
190 # Notice facets form must be rendered **outside** the main div as it |
|
191 # shouldn't be rendered on ajax call subsequent to facet restriction |
|
192 # (hence the 'fromformfilter' parameter added by the form |
|
193 generate_form = self.initial_load |
|
194 if self.display_filter and generate_form: |
|
195 facetsform = self.view.facets_form() |
|
196 else: |
|
197 facetsform = None |
|
198 if facetsform and self.display_filter == 'top': |
|
199 cssclass = u'hidden' if self.hide_filter else u'' |
|
200 facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, |
|
201 divid=self.view.domid) |
|
202 actions = [] |
|
203 if self.add_view_actions: |
|
204 actions = self.view.table_actions() |
|
205 if self.display_filter and self.hide_filter and (facetsform or not generate_form): |
|
206 actions += self.show_hide_filter_actions(not generate_form) |
|
207 self.render_table(w, actions, self.view.paginable) |
|
208 if facetsform and self.display_filter == 'bottom': |
|
209 cssclass = u'hidden' if self.hide_filter else u'' |
|
210 facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, |
|
211 divid=self.view.domid) |
|
212 |
|
213 def render_table_headers(self, w, colrenderers): |
|
214 w(u'<thead><tr>') |
|
215 for colrenderer in colrenderers: |
|
216 if colrenderer.sortable: |
|
217 w(u'<th class="sortable">') |
|
218 else: |
|
219 w(u'<th>') |
|
220 colrenderer.render_header(w) |
|
221 w(u'</th>') |
|
222 w(u'</tr></thead>\n') |
|
223 |
|
224 def render_table_body(self, w, colrenderers): |
|
225 w(u'<tbody>') |
|
226 for rownum in xrange(self.view.table_size): |
|
227 self.render_row(w, rownum, colrenderers) |
|
228 w(u'</tbody>') |
|
229 |
|
230 def render_table(self, w, actions, paginate): |
|
231 view = self.view |
|
232 divid = view.domid |
|
233 if divid is not None: |
|
234 w(u'<div id="%s">' % divid) |
|
235 else: |
|
236 assert not (actions or paginate) |
|
237 nav_html = UStringIO() |
|
238 if paginate: |
|
239 view.paginate(w=nav_html.write, show_all_option=self.show_all_option) |
|
240 w(nav_html.getvalue()) |
|
241 if actions and self.display_actions == 'top': |
|
242 self.render_actions(w, actions) |
|
243 colrenderers = view.build_column_renderers() |
|
244 attrs = self.table_attributes() |
|
245 w(u'<table %s>' % sgml_attributes(attrs)) |
|
246 if self.view.has_headers: |
|
247 self.render_table_headers(w, colrenderers) |
|
248 self.render_table_body(w, colrenderers) |
|
249 w(u'</table>') |
|
250 if actions and self.display_actions == 'bottom': |
|
251 self.render_actions(w, actions) |
|
252 w(nav_html.getvalue()) |
|
253 if divid is not None: |
|
254 w(u'</div>') |
|
255 |
|
256 def table_attributes(self): |
|
257 return {'class': self.cssclass} |
|
258 |
|
259 def render_row(self, w, rownum, renderers): |
|
260 attrs = self.row_attributes(rownum) |
|
261 w(u'<tr %s>' % sgml_attributes(attrs)) |
|
262 for colnum, renderer in enumerate(renderers): |
|
263 self.render_cell(w, rownum, colnum, renderer) |
|
264 w(u'</tr>\n') |
|
265 |
|
266 def row_attributes(self, rownum): |
|
267 return {'class': 'odd' if (rownum%2==1) else 'even', |
|
268 'onmouseover': '$(this).addClass("highlighted");', |
|
269 'onmouseout': '$(this).removeClass("highlighted")'} |
|
270 |
|
271 def render_cell(self, w, rownum, colnum, renderer): |
|
272 attrs = self.cell_attributes(rownum, colnum, renderer) |
|
273 if colnum in self.header_column_idx: |
|
274 tag = u'th' |
|
275 else: |
|
276 tag = u'td' |
|
277 w(u'<%s %s>' % (tag, sgml_attributes(attrs))) |
|
278 renderer.render_cell(w, rownum) |
|
279 w(u'</%s>' % tag) |
|
280 |
|
281 def cell_attributes(self, rownum, _colnum, renderer): |
|
282 attrs = renderer.attributes.copy() |
|
283 if renderer.sortable: |
|
284 sortvalue = renderer.sortvalue(rownum) |
|
285 if isinstance(sortvalue, basestring): |
|
286 sortvalue = sortvalue[:self.sortvalue_limit] |
|
287 if sortvalue is not None: |
|
288 attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue) |
|
289 return attrs |
|
290 |
|
291 def render_actions(self, w, actions): |
|
292 box = MenuWidget('', '', _class='tableActionsBox', islist=False) |
|
293 label = tags.span(self._cw._('action menu')) |
|
294 menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', |
|
295 ident='%sActions' % self.view.domid) |
|
296 box.append(menu) |
|
297 for action in actions: |
|
298 menu.append(action) |
|
299 box.render(w=w) |
|
300 w(u'<div class="clear"></div>') |
|
301 |
|
302 def show_hide_filter_actions(self, currentlydisplayed=False): |
|
303 divid = self.view.domid |
|
304 showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] |
|
305 for what in ('Form', 'Show', 'Hide', 'Actions')) |
|
306 showhide = 'javascript:' + showhide |
|
307 self._cw.add_onload(u'''\ |
|
308 $(document).ready(function() { |
|
309 if ($('#%(id)sForm[class=\"hidden\"]').length) { |
|
310 $('#%(id)sHide').attr('class', 'hidden'); |
|
311 } else { |
|
312 $('#%(id)sShow').attr('class', 'hidden'); |
|
313 } |
|
314 });''' % {'id': divid}) |
|
315 showlabel = self._cw._('show filter form') |
|
316 hidelabel = self._cw._('hide filter form') |
|
317 return [component.Link(showhide, showlabel, id='%sShow' % divid), |
|
318 component.Link(showhide, hidelabel, id='%sHide' % divid)] |
|
319 |
|
320 |
|
321 class AbstractColumnRenderer(object): |
|
322 """Abstract base class for column renderer. Interface of a column renderer follows: |
|
323 |
|
324 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.bind |
|
325 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_header |
|
326 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.render_cell |
|
327 .. automethod:: cubicweb.web.views.tableview.AbstractColumnRenderer.sortvalue |
|
328 |
|
329 Attributes on this base class are: |
|
330 |
|
331 :attr: `header`, the column header. If None, default to `_(colid)` |
|
332 :attr: `addcount`, if True, add the table size in parenthezis beside the header |
|
333 :attr: `trheader`, should the header be translated |
|
334 :attr: `escapeheader`, should the header be xml_escaped |
|
335 :attr: `sortable`, tell if the column is sortable |
|
336 :attr: `view`, the table view |
|
337 :attr: `_cw`, the request object |
|
338 :attr: `colid`, the column identifier |
|
339 :attr: `attributes`, dictionary of attributes to put on the HTML tag when |
|
340 the cell is rendered |
|
341 """ #'# make emacs |
|
342 attributes = {} |
|
343 empty_cell_content = u' ' |
|
344 |
|
345 def __init__(self, header=None, addcount=False, trheader=True, |
|
346 escapeheader=True, sortable=True): |
|
347 self.header = header |
|
348 self.trheader = trheader |
|
349 self.escapeheader = escapeheader |
|
350 self.addcount = addcount |
|
351 self.sortable = sortable |
|
352 self.view = None |
|
353 self._cw = None |
|
354 self.colid = None |
|
355 |
|
356 def __str__(self): |
|
357 return '<%s.%s (column %s)>' % (self.view.__class__.__name__, |
|
358 self.__class__.__name__, |
|
359 self.colid) |
|
360 |
|
361 def bind(self, view, colid): |
|
362 """Bind the column renderer to its view. This is where `_cw`, `view`, |
|
363 `colid` are set and the method to override if you want to add more |
|
364 view/request depending attributes on your column render. |
|
365 """ |
|
366 self.view = view |
|
367 self._cw = view._cw |
|
368 self.colid = colid |
|
369 |
|
370 def copy(self): |
|
371 assert self.view is None |
|
372 return copy(self) |
|
373 |
|
374 def default_header(self): |
|
375 """Return header for this column if one has not been specified.""" |
|
376 return self._cw._(self.colid) |
|
377 |
|
378 def render_header(self, w): |
|
379 """Write label for the specified column by calling w().""" |
|
380 header = self.header |
|
381 if header is None: |
|
382 header = self.default_header() |
|
383 elif self.trheader and header: |
|
384 header = self._cw._(header) |
|
385 if self.addcount: |
|
386 header = '%s (%s)' % (header, self.view.table_size) |
|
387 if header: |
|
388 if self.escapeheader: |
|
389 header = xml_escape(header) |
|
390 else: |
|
391 header = self.empty_cell_content |
|
392 if self.sortable: |
|
393 header = tags.span( |
|
394 header, escapecontent=False, |
|
395 title=self._cw._('Click to sort on this column')) |
|
396 w(header) |
|
397 |
|
398 def render_cell(self, w, rownum): |
|
399 """Write value for the specified cell by calling w(). |
|
400 |
|
401 :param `rownum`: the row number in the table |
|
402 """ |
|
403 raise NotImplementedError() |
|
404 |
|
405 def sortvalue(self, _rownum): |
|
406 """Return typed value to be used for sorting on the specified column. |
|
407 |
|
408 :param `rownum`: the row number in the table |
|
409 """ |
|
410 return None |
|
411 |
|
412 |
|
413 class TableMixIn(component.LayoutableMixIn): |
|
414 """Abstract mix-in class for layout based tables. |
|
415 |
|
416 This default implementation's call method simply delegate to |
|
417 meth:`layout_render` that will select the renderer whose identifier is given |
|
418 by the :attr:`layout_id` attribute. |
|
419 |
|
420 Then it provides some default implementation for various parts of the API |
|
421 used by that layout. |
|
422 |
|
423 Abstract method you will have to override is: |
|
424 |
|
425 .. automethod:: build_column_renderers |
|
426 |
|
427 You may also want to overridde: |
|
428 |
|
429 .. autoattribute:: cubicweb.web.views.tableview.TableMixIn.table_size |
|
430 |
|
431 The :attr:`has_headers` boolean attribute tells if the table has some |
|
432 headers to be displayed. Default to `True`. |
|
433 """ |
|
434 __abstract__ = True |
|
435 # table layout to use |
|
436 layout_id = 'table_layout' |
|
437 # true if the table has some headers |
|
438 has_headers = True |
|
439 # dictionary {colid : column renderer} |
|
440 column_renderers = {} |
|
441 # default renderer class to use when no renderer specified for the column |
|
442 default_column_renderer_class = None |
|
443 # default layout handles inner pagination |
|
444 handle_pagination = True |
|
445 |
|
446 def call(self, **kwargs): |
|
447 self.layout_render(self.w) |
|
448 |
|
449 def column_renderer(self, colid, *args, **kwargs): |
|
450 """Return a column renderer for column of the given id.""" |
|
451 try: |
|
452 crenderer = self.column_renderers[colid] |
|
453 except KeyError: |
|
454 crenderer = self.default_column_renderer_class(*args, **kwargs) |
|
455 crenderer.bind(self, colid) |
|
456 return crenderer |
|
457 |
|
458 # layout callbacks ######################################################### |
|
459 |
|
460 def facets_form(self, **kwargs):# XXX extracted from jqplot cube |
|
461 try: |
|
462 return self._cw.vreg['views'].select( |
|
463 'facet.filtertable', self._cw, rset=self.cw_rset, view=self, |
|
464 **kwargs) |
|
465 except NoSelectableObject: |
|
466 return None |
|
467 |
|
468 @cachedproperty |
|
469 def domid(self): |
|
470 return self._cw.form.get('divid') or domid('%s-%s' % (self.__regid__, make_uid())) |
|
471 |
|
472 @property |
|
473 def table_size(self): |
|
474 """Return the number of rows (header excluded) to be displayed. |
|
475 |
|
476 By default return the number of rows in the view's result set. If your |
|
477 table isn't reult set based, override this method. |
|
478 """ |
|
479 return self.cw_rset.rowcount |
|
480 |
|
481 def build_column_renderers(self): |
|
482 """Return a list of column renderers, one for each column to be |
|
483 rendered. Prototype of a column renderer is described below: |
|
484 |
|
485 .. autoclass:: cubicweb.web.views.tableview.AbstractColumnRenderer |
|
486 """ |
|
487 raise NotImplementedError() |
|
488 |
|
489 def table_actions(self): |
|
490 """Return a list of actions (:class:`~cubicweb.web.component.Link`) that |
|
491 match the view's result set, and return those in the 'mainactions' |
|
492 category. |
|
493 """ |
|
494 req = self._cw |
|
495 actions = [] |
|
496 actionsbycat = req.vreg['actions'].possible_actions(req, self.cw_rset) |
|
497 for action in actionsbycat.get('mainactions', ()): |
|
498 for action in action.actual_actions(): |
|
499 actions.append(component.Link(action.url(), req._(action.title), |
|
500 klass=action.html_class()) ) |
|
501 return actions |
|
502 |
|
503 # interaction with navigation component #################################### |
|
504 |
|
505 def page_navigation_url(self, navcomp, _path, params): |
|
506 params['divid'] = self.domid |
|
507 params['vid'] = self.__regid__ |
|
508 return navcomp.ajax_page_url(**params) |
|
509 |
|
510 |
|
511 class RsetTableColRenderer(AbstractColumnRenderer): |
|
512 """Default renderer for :class:`RsetTableView`.""" |
|
513 |
|
514 def __init__(self, cellvid, **kwargs): |
|
515 super(RsetTableColRenderer, self).__init__(**kwargs) |
|
516 self.cellvid = cellvid |
|
517 |
|
518 def bind(self, view, colid): |
|
519 super(RsetTableColRenderer, self).bind(view, colid) |
|
520 self.cw_rset = view.cw_rset |
|
521 def render_cell(self, w, rownum): |
|
522 self._cw.view(self.cellvid, self.cw_rset, 'empty-cell', |
|
523 row=rownum, col=self.colid, w=w) |
|
524 |
|
525 # limit value's length as much as possible (e.g. by returning the 10 first |
|
526 # characters of a string) |
|
527 def sortvalue(self, rownum): |
|
528 colid = self.colid |
|
529 val = self.cw_rset[rownum][colid] |
|
530 if val is None: |
|
531 return u'' |
|
532 etype = self.cw_rset.description[rownum][colid] |
|
533 if etype is None: |
|
534 return u'' |
|
535 if self._cw.vreg.schema.eschema(etype).final: |
|
536 entity, rtype = self.cw_rset.related_entity(rownum, colid) |
|
537 if entity is None: |
|
538 return val # remove_html_tags() ? |
|
539 return entity.sortvalue(rtype) |
|
540 entity = self.cw_rset.get_entity(rownum, colid) |
|
541 return entity.sortvalue() |
|
542 |
|
543 |
|
544 class RsetTableView(TableMixIn, AnyRsetView): |
|
545 """This table view accepts any non-empty rset. It uses introspection on the |
|
546 result set to compute column names and the proper way to display the cells. |
|
547 |
|
548 It is highly configurable and accepts a wealth of options, but take care to |
|
549 check what you're trying to achieve wouldn't be a job for the |
|
550 :class:`EntityTableView`. Basically the question is: does this view should |
|
551 be tied to the result set query's shape or no? If yes, than you're fine. If |
|
552 no, you should take a look at the other table implementation. |
|
553 |
|
554 The following class attributes may be used to control the table: |
|
555 |
|
556 * `finalvid`, a view identifier that should be called on final entities |
|
557 (e.g. attribute values). Default to 'final'. |
|
558 |
|
559 * `nonfinalvid`, a view identifier that should be called on |
|
560 entities. Default to 'incontext'. |
|
561 |
|
562 * `displaycols`, if not `None`, should be a list of rset's columns to be |
|
563 displayed. |
|
564 |
|
565 * `headers`, if not `None`, should be a list of headers for the table's |
|
566 columns. `None` values in the list will be replaced by computed column |
|
567 names. |
|
568 |
|
569 * `cellvids`, if not `None`, should be a dictionary with table column index |
|
570 as key and a view identifier as value, telling the view that should be |
|
571 used in the given column. |
|
572 |
|
573 Notice `displaycols`, `headers` and `cellvids` may be specified at selection |
|
574 time but then the table won't have pagination and shouldn't be configured to |
|
575 display the facets filter nor actions (as they wouldn't behave as expected). |
|
576 |
|
577 This table class use the :class:`RsetTableColRenderer` as default column |
|
578 renderer. |
|
579 |
|
580 .. autoclass:: RsetTableColRenderer |
|
581 """ #'# make emacs happier |
|
582 __regid__ = 'table' |
|
583 # selector trick for bw compath with the former :class:TableView |
|
584 __select__ = AnyRsetView.__select__ & (~match_kwargs( |
|
585 'title', 'subvid', 'displayfilter', 'headers', 'displaycols', |
|
586 'displayactions', 'actions', 'divid', 'cellvids', 'cellattrs', |
|
587 'mainindex', 'paginate', 'page_size', mode='any') |
|
588 | unreloadable_table()) |
|
589 title = _('table') |
|
590 # additional configuration parameters |
|
591 finalvid = 'final' |
|
592 nonfinalvid = 'incontext' |
|
593 displaycols = None |
|
594 headers = None |
|
595 cellvids = None |
|
596 default_column_renderer_class = RsetTableColRenderer |
|
597 |
|
598 def linkable(self): |
|
599 # specific subclasses of this view usually don't want to be linkable |
|
600 # since they depends on a particular shape (being linkable meaning view |
|
601 # may be listed in possible views |
|
602 return self.__regid__ == 'table' |
|
603 |
|
604 def call(self, headers=None, displaycols=None, cellvids=None, |
|
605 paginate=None, **kwargs): |
|
606 if self.headers: |
|
607 self.headers = [h and self._cw._(h) for h in self.headers] |
|
608 if (headers or displaycols or cellvids or paginate): |
|
609 if headers is not None: |
|
610 self.headers = headers |
|
611 if displaycols is not None: |
|
612 self.displaycols = displaycols |
|
613 if cellvids is not None: |
|
614 self.cellvids = cellvids |
|
615 if paginate is not None: |
|
616 self.paginable = paginate |
|
617 if kwargs: |
|
618 # old table view arguments that we can safely ignore thanks to |
|
619 # selectors |
|
620 if len(kwargs) > 1: |
|
621 msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs) |
|
622 else: |
|
623 msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs) |
|
624 warn(msg, DeprecationWarning, stacklevel=2) |
|
625 self.layout_render(self.w) |
|
626 |
|
627 def main_var_index(self): |
|
628 """returns the index of the first non-attribute variable among the RQL |
|
629 selected variables |
|
630 """ |
|
631 eschema = self._cw.vreg.schema.eschema |
|
632 for i, etype in enumerate(self.cw_rset.description[0]): |
|
633 if not eschema(etype).final: |
|
634 return i |
|
635 return None |
|
636 |
|
637 # layout callbacks ######################################################### |
|
638 |
|
639 @property |
|
640 def table_size(self): |
|
641 """return the number of rows (header excluded) to be displayed""" |
|
642 return self.cw_rset.rowcount |
|
643 |
|
644 def build_column_renderers(self): |
|
645 headers = self.headers |
|
646 # compute displayed columns |
|
647 if self.displaycols is None: |
|
648 if headers is not None: |
|
649 displaycols = range(len(headers)) |
|
650 else: |
|
651 rqlst = self.cw_rset.syntax_tree() |
|
652 displaycols = range(len(rqlst.children[0].selection)) |
|
653 else: |
|
654 displaycols = self.displaycols |
|
655 # compute table headers |
|
656 main_var_index = self.main_var_index() |
|
657 computed_titles = self.columns_labels(main_var_index) |
|
658 # compute build renderers |
|
659 cellvids = self.cellvids |
|
660 renderers = [] |
|
661 for colnum, colid in enumerate(displaycols): |
|
662 addcount = False |
|
663 # compute column header |
|
664 title = None |
|
665 if headers is not None: |
|
666 title = headers[colnum] |
|
667 if title is None: |
|
668 title = computed_titles[colid] |
|
669 if colid == main_var_index: |
|
670 addcount = True |
|
671 # compute cell vid for the column |
|
672 if cellvids is not None and colnum in cellvids: |
|
673 cellvid = cellvids[colnum] |
|
674 else: |
|
675 coltype = self.cw_rset.description[0][colid] |
|
676 if coltype is not None and self._cw.vreg.schema.eschema(coltype).final: |
|
677 cellvid = self.finalvid |
|
678 else: |
|
679 cellvid = self.nonfinalvid |
|
680 # get renderer |
|
681 renderer = self.column_renderer(colid, header=title, trheader=False, |
|
682 addcount=addcount, cellvid=cellvid) |
|
683 renderers.append(renderer) |
|
684 return renderers |
|
685 |
|
686 |
|
687 class EntityTableColRenderer(AbstractColumnRenderer): |
|
688 """Default column renderer for :class:`EntityTableView`. |
|
689 |
|
690 You may use the :meth:`entity` method to retrieve the main entity for a |
|
691 given row number. |
|
692 |
|
693 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity |
|
694 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.render_entity |
|
695 .. automethod:: cubicweb.web.views.tableview.EntityTableColRenderer.entity_sortvalue |
|
696 """ |
|
697 def __init__(self, renderfunc=None, sortfunc=None, sortable=None, **kwargs): |
|
698 if renderfunc is None: |
|
699 renderfunc = self.render_entity |
|
700 # if renderfunc nor sortfunc nor sortable specified, column will be |
|
701 # sortable using the default implementation. |
|
702 if sortable is None: |
|
703 sortable = True |
|
704 # no sortfunc given but asked to be sortable: use the default sort |
|
705 # method. Sub-class may set `entity_sortvalue` to None if they don't |
|
706 # support sorting. |
|
707 if sortfunc is None and sortable: |
|
708 sortfunc = self.entity_sortvalue |
|
709 # at this point `sortable` may still be unspecified while `sortfunc` is |
|
710 # sure to be set to someting else than None if the column is sortable. |
|
711 sortable = sortfunc is not None |
|
712 super(EntityTableColRenderer, self).__init__(sortable=sortable, **kwargs) |
|
713 self.renderfunc = renderfunc |
|
714 self.sortfunc = sortfunc |
|
715 |
|
716 def copy(self): |
|
717 assert self.view is None |
|
718 # copy of attribute referencing a method doesn't work with python < 2.7 |
|
719 renderfunc = self.__dict__.pop('renderfunc') |
|
720 sortfunc = self.__dict__.pop('sortfunc') |
|
721 try: |
|
722 acopy = copy(self) |
|
723 for aname, member in[('renderfunc', renderfunc), |
|
724 ('sortfunc', sortfunc)]: |
|
725 if isinstance(member, MethodType): |
|
726 member = MethodType(member.im_func, acopy, acopy.__class__) |
|
727 setattr(acopy, aname, member) |
|
728 return acopy |
|
729 finally: |
|
730 self.renderfunc = renderfunc |
|
731 self.sortfunc = sortfunc |
|
732 |
|
733 def render_cell(self, w, rownum): |
|
734 entity = self.entity(rownum) |
|
735 if entity is None: |
|
736 w(self.empty_cell_content) |
|
737 else: |
|
738 self.renderfunc(w, entity) |
|
739 |
|
740 def sortvalue(self, rownum): |
|
741 entity = self.entity(rownum) |
|
742 if entity is None: |
|
743 return None |
|
744 else: |
|
745 return self.sortfunc(entity) |
|
746 |
|
747 def entity(self, rownum): |
|
748 """Convenience method returning the table's main entity.""" |
|
749 return self.view.entity(rownum) |
|
750 |
|
751 def render_entity(self, w, entity): |
|
752 """Sort value if `renderfunc` nor `sortfunc` specified at |
|
753 initialization. |
|
754 |
|
755 This default implementation consider column id is an entity attribute |
|
756 and print its value. |
|
757 """ |
|
758 w(entity.printable_value(self.colid)) |
|
759 |
|
760 def entity_sortvalue(self, entity): |
|
761 """Cell rendering implementation if `renderfunc` nor `sortfunc` |
|
762 specified at initialization. |
|
763 |
|
764 This default implementation consider column id is an entity attribute |
|
765 and return its sort value by calling `entity.sortvalue(colid)`. |
|
766 """ |
|
767 return entity.sortvalue(self.colid) |
|
768 |
|
769 |
|
770 class MainEntityColRenderer(EntityTableColRenderer): |
|
771 """Renderer to be used for the column displaying the 'main entity' of a |
|
772 :class:`EntityTableView`. |
|
773 |
|
774 By default display it using the 'incontext' view. You may specify another |
|
775 view identifier using the `vid` argument. |
|
776 |
|
777 If header not specified, it would be built using entity types in the main |
|
778 column. |
|
779 """ |
|
780 def __init__(self, vid='incontext', addcount=True, **kwargs): |
|
781 super(MainEntityColRenderer, self).__init__(addcount=addcount, **kwargs) |
|
782 self.vid = vid |
|
783 |
|
784 def default_header(self): |
|
785 view = self.view |
|
786 if len(view.cw_rset) > 1: |
|
787 suffix = '_plural' |
|
788 else: |
|
789 suffix = '' |
|
790 return u', '.join(self._cw.__(et + suffix) |
|
791 for et in view.cw_rset.column_types(view.cw_col or 0)) |
|
792 |
|
793 def render_entity(self, w, entity): |
|
794 entity.view(self.vid, w=w) |
|
795 |
|
796 def entity_sortvalue(self, entity): |
|
797 return entity.sortvalue() |
|
798 |
|
799 |
|
800 class RelatedEntityColRenderer(MainEntityColRenderer): |
|
801 """Renderer to be used for column displaying an entity related the 'main |
|
802 entity' of a :class:`EntityTableView`. |
|
803 |
|
804 By default display it using the 'incontext' view. You may specify another |
|
805 view identifier using the `vid` argument. |
|
806 |
|
807 If header not specified, it would be built by translating the column id. |
|
808 """ |
|
809 def __init__(self, getrelated, addcount=False, **kwargs): |
|
810 super(RelatedEntityColRenderer, self).__init__(addcount=addcount, **kwargs) |
|
811 self.getrelated = getrelated |
|
812 |
|
813 def entity(self, rownum): |
|
814 entity = super(RelatedEntityColRenderer, self).entity(rownum) |
|
815 return self.getrelated(entity) |
|
816 |
|
817 def default_header(self): |
|
818 return self._cw._(self.colid) |
|
819 |
|
820 |
|
821 class RelationColRenderer(EntityTableColRenderer): |
|
822 """Renderer to be used for column displaying a list of entities related the |
|
823 'main entity' of a :class:`EntityTableView`. By default, the main entity is |
|
824 considered as the subject of the relation but you may specify otherwise |
|
825 using the `role` argument. |
|
826 |
|
827 By default display the related rset using the 'csv' view, using |
|
828 'outofcontext' sub-view for each entity. You may specify another view |
|
829 identifier using respectivly the `vid` and `subvid` arguments. |
|
830 |
|
831 If you specify a 'rtype view', such as 'reledit', you should add a |
|
832 is_rtype_view=True parameter. |
|
833 |
|
834 If header not specified, it would be built by translating the column id, |
|
835 properly considering role. |
|
836 """ |
|
837 def __init__(self, role='subject', vid='csv', subvid=None, |
|
838 fallbackvid='empty-cell', is_rtype_view=False, **kwargs): |
|
839 super(RelationColRenderer, self).__init__(**kwargs) |
|
840 self.role = role |
|
841 self.vid = vid |
|
842 if subvid is None and vid in ('csv', 'list'): |
|
843 subvid = 'outofcontext' |
|
844 self.subvid = subvid |
|
845 self.fallbackvid = fallbackvid |
|
846 self.is_rtype_view = is_rtype_view |
|
847 |
|
848 def render_entity(self, w, entity): |
|
849 kwargs = {'w': w} |
|
850 if self.is_rtype_view: |
|
851 rset = None |
|
852 kwargs['entity'] = entity |
|
853 kwargs['rtype'] = self.colid |
|
854 kwargs['role'] = self.role |
|
855 else: |
|
856 rset = entity.related(self.colid, self.role) |
|
857 if self.subvid is not None: |
|
858 kwargs['subvid'] = self.subvid |
|
859 self._cw.view(self.vid, rset, self.fallbackvid, **kwargs) |
|
860 |
|
861 def default_header(self): |
|
862 return display_name(self._cw, self.colid, self.role) |
|
863 |
|
864 entity_sortvalue = None # column not sortable by default |
|
865 |
|
866 |
|
867 class EntityTableView(TableMixIn, EntityView): |
|
868 """This abstract table view is designed to be used with an |
|
869 :class:`is_instance()` or :class:`adaptable` selector, hence doesn't depend |
|
870 the result set shape as the :class:`TableView` does. |
|
871 |
|
872 It will display columns that should be defined using the `columns` class |
|
873 attribute containing a list of column ids. By default, each column is |
|
874 renderered by :class:`EntityTableColRenderer` which consider that the column |
|
875 id is an attribute of the table's main entity (ie the one for which the view |
|
876 is selected). |
|
877 |
|
878 You may wish to specify :class:`MainEntityColRenderer` or |
|
879 :class:`RelatedEntityColRenderer` renderer for a column in the |
|
880 :attr:`column_renderers` dictionary. |
|
881 |
|
882 .. autoclass:: cubicweb.web.views.tableview.EntityTableColRenderer |
|
883 .. autoclass:: cubicweb.web.views.tableview.MainEntityColRenderer |
|
884 .. autoclass:: cubicweb.web.views.tableview.RelatedEntityColRenderer |
|
885 .. autoclass:: cubicweb.web.views.tableview.RelationColRenderer |
|
886 """ |
|
887 __abstract__ = True |
|
888 default_column_renderer_class = EntityTableColRenderer |
|
889 columns = None # to be defined in concret class |
|
890 |
|
891 def call(self, columns=None, **kwargs): |
|
892 if columns is not None: |
|
893 self.columns = columns |
|
894 self.layout_render(self.w) |
|
895 |
|
896 @property |
|
897 def table_size(self): |
|
898 return self.cw_rset.rowcount |
|
899 |
|
900 def build_column_renderers(self): |
|
901 return [self.column_renderer(colid) for colid in self.columns] |
|
902 |
|
903 def entity(self, rownum): |
|
904 """Return the table's main entity""" |
|
905 return self.cw_rset.get_entity(rownum, self.cw_col or 0) |
|
906 |
|
907 |
|
908 class EmptyCellView(AnyRsetView): |
|
909 __regid__ = 'empty-cell' |
|
910 __select__ = yes() |
|
911 def call(self, **kwargs): |
|
912 self.w(u' ') |
|
913 cell_call = call |
|
914 |
|
915 |
|
916 ################################################################################ |
|
917 # DEPRECATED tables ############################################################ |
|
918 ################################################################################ |
33 |
919 |
34 |
920 |
35 class TableView(AnyRsetView): |
921 class TableView(AnyRsetView): |
36 """The table view accepts any non-empty rset. It uses introspection on the |
922 """The table view accepts any non-empty rset. It uses introspection on the |
37 result set to compute column names and the proper way to display the cells. |
923 result set to compute column names and the proper way to display the cells. |
38 |
924 |
39 It is however highly configurable and accepts a wealth of options. |
925 It is however highly configurable and accepts a wealth of options. |
40 """ |
926 """ |
|
927 __metaclass__ = class_deprecated |
|
928 __deprecation_warning__ = '[3.14] %(cls)s is deprecated' |
41 __regid__ = 'table' |
929 __regid__ = 'table' |
42 title = _('table') |
930 title = _('table') |
43 finalview = 'final' |
931 finalview = 'final' |
44 |
932 |
45 table_widget_class = TableWidget |
933 table_widget_class = TableWidget |
46 table_column_class = TableColumn |
934 table_column_class = TableColumn |
47 |
935 |
48 tablesorter_settings = { |
936 tablesorter_settings = { |
49 'textExtraction': JSString('cubicwebSortValueExtraction'), |
937 'textExtraction': JSString('cw.sortValueExtraction'), |
|
938 'selectorHeaders': 'thead tr:first th', # only plug on the first row |
50 } |
939 } |
|
940 handle_pagination = True |
51 |
941 |
52 def form_filter(self, divid, displaycols, displayactions, displayfilter, |
942 def form_filter(self, divid, displaycols, displayactions, displayfilter, |
53 paginate, hidden=True): |
943 paginate, hidden=True): |
54 try: |
944 try: |
55 filterform = self._cw.vreg['views'].select( |
945 filterform = self._cw.vreg['views'].select( |