|
1 """generic table view, including filtering abilities |
|
2 |
|
3 |
|
4 :organization: Logilab |
|
5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
7 """ |
|
8 __docformat__ = "restructuredtext en" |
|
9 |
|
10 from simplejson import dumps |
|
11 |
|
12 from logilab.mtconverter import html_escape |
|
13 |
|
14 from cubicweb.common.utils import make_uid |
|
15 from cubicweb.common.uilib import toggle_action, limitsize, jsonize, htmlescape |
|
16 from cubicweb.common.view import EntityView, AnyRsetView |
|
17 from cubicweb.common.selectors import (anyrset_selector, req_form_params_selector, |
|
18 accept_rset_selector) |
|
19 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget, |
|
20 PopupBoxMenu, BoxLink) |
|
21 from cubicweb.web.facet import prepare_facets_rqlst, filter_hiddens |
|
22 |
|
23 class TableView(AnyRsetView): |
|
24 id = 'table' |
|
25 title = _('table') |
|
26 finalview = 'final' |
|
27 |
|
28 def generate_form(self, divid, baserql, facets, hidden=True, vidargs={}): |
|
29 """display a form to filter table's content. This should only |
|
30 occurs when a context eid is given |
|
31 """ |
|
32 self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.formfilter.js')) |
|
33 # drop False / None values from vidargs |
|
34 vidargs = dict((k, v) for k, v in vidargs.iteritems() if v) |
|
35 self.w(u'<form method="post" cubicweb:facetargs="%s" action="">' % |
|
36 html_escape(dumps([divid, 'table', False, vidargs]))) |
|
37 self.w(u'<fieldset id="%sForm" class="%s">' % (divid, hidden and 'hidden' or '')) |
|
38 self.w(u'<input type="hidden" name="divid" value="%s" />' % divid) |
|
39 filter_hiddens(self.w, facets=','.join(facet.id for facet in facets), baserql=baserql) |
|
40 self.w(u'<table class="filter">\n') |
|
41 self.w(u'<tr>\n') |
|
42 for facet in facets: |
|
43 wdg = facet.get_widget() |
|
44 print 'FACT WIDGET', wdg |
|
45 if wdg is not None: |
|
46 self.w(u'<td>') |
|
47 wdg.render(w=self.w) |
|
48 self.w(u'</td>\n') |
|
49 self.w(u'</tr>\n') |
|
50 self.w(u'</table>\n') |
|
51 self.w(u'</fieldset>\n') |
|
52 self.w(u'</form>\n') |
|
53 |
|
54 def main_var_index(self): |
|
55 """returns the index of the first non-attribute variable among the RQL |
|
56 selected variables |
|
57 """ |
|
58 eschema = self.vreg.schema.eschema |
|
59 for i, etype in enumerate(self.rset.description[0]): |
|
60 try: |
|
61 if not eschema(etype).is_final(): |
|
62 return i |
|
63 except KeyError: # XXX possible? |
|
64 continue |
|
65 return None |
|
66 |
|
67 def displaycols(self, displaycols): |
|
68 if displaycols is None: |
|
69 if 'displaycols' in self.req.form: |
|
70 displaycols = [int(idx) for idx in self.req.form['displaycols']] |
|
71 else: |
|
72 displaycols = range(len(self.rset.syntax_tree().children[0].selection)) |
|
73 return displaycols |
|
74 |
|
75 def call(self, title=None, subvid=None, displayfilter=None, headers=None, |
|
76 displaycols=None, displayactions=None, actions=(), |
|
77 cellvids=None, cellattrs=None): |
|
78 """Dumps a table displaying a composite query |
|
79 |
|
80 :param title: title added before table |
|
81 :param subvid: cell view |
|
82 :param displayfilter: filter that selects rows to display |
|
83 :param headers: columns' titles |
|
84 """ |
|
85 rset = self.rset |
|
86 req = self.req |
|
87 req.add_js('jquery.tablesorter.js') |
|
88 req.add_css('cubicweb.tablesorter.css') |
|
89 rqlst = rset.syntax_tree() |
|
90 # get rql description first since the filter form may remove some |
|
91 # necessary information |
|
92 rqlstdescr = rqlst.get_description()[0] # XXX missing Union support |
|
93 mainindex = self.main_var_index() |
|
94 hidden = True |
|
95 if not subvid and 'subvid' in req.form: |
|
96 subvid = req.form.pop('subvid') |
|
97 divid = req.form.get('divid') or 'rs%s' % make_uid(id(rset)) |
|
98 actions = list(actions) |
|
99 if mainindex is None: |
|
100 displayfilter, displayactions = False, False |
|
101 else: |
|
102 if displayfilter is None and 'displayfilter' in req.form: |
|
103 displayfilter = True |
|
104 if req.form['displayfilter'] == 'shown': |
|
105 hidden = False |
|
106 if displayactions is None and 'displayactions' in req.form: |
|
107 displayactions = True |
|
108 displaycols = self.displaycols(displaycols) |
|
109 fromformfilter = 'fromformfilter' in req.form |
|
110 # if fromformfilter is true, this is an ajax call and we only want to |
|
111 # replace the inner div, so don't regenerate everything under the if |
|
112 # below |
|
113 if not fromformfilter: |
|
114 div_class = 'section' |
|
115 self.w(u'<div class="%s">' % div_class) |
|
116 if not title and 'title' in req.form: |
|
117 title = req.form['title'] |
|
118 if title: |
|
119 self.w(u'<h2 class="tableTitle">%s</h2>\n' % title) |
|
120 if displayfilter: |
|
121 rqslt.save_state() |
|
122 try: |
|
123 mainvar, baserql = prepare_facets_rqlst(rqlst, rset.args) |
|
124 except NotImplementedError: |
|
125 # UNION query |
|
126 facets = None |
|
127 else: |
|
128 facets = list(self.vreg.possible_vobjects('facets', req, rset, |
|
129 context='tablefilter', |
|
130 filtered_variable=mainvar)) |
|
131 self.generate_form(divid, baserql, facets, hidden, |
|
132 vidargs={'displaycols': displaycols, |
|
133 'displayfilter': displayfilter, |
|
134 'displayactions': displayactions}) |
|
135 actions += self.show_hide_actions(divid, not hidden) |
|
136 rqlst.recover() |
|
137 elif displayfilter: |
|
138 actions += self.show_hide_actions(divid, True) |
|
139 self.w(u'<div id="%s"' % divid) |
|
140 if displayactions: |
|
141 for action in self.vreg.possible_actions(req, self.rset).get('mainactions', ()): |
|
142 actions.append( (action.url(), req._(action.title), action.html_class(), None) ) |
|
143 self.w(u' cubicweb:displayactions="1">') # close <div tag |
|
144 else: |
|
145 self.w(u'>') # close <div tag |
|
146 # render actions menu |
|
147 if actions: |
|
148 self.render_actions(divid, actions) |
|
149 # render table |
|
150 table = TableWidget(self) |
|
151 for column in self.get_columns(rqlstdescr, displaycols, headers, subvid, |
|
152 cellvids, cellattrs, mainindex): |
|
153 table.append_column(column) |
|
154 table.render(self.w) |
|
155 self.w(u'</div>\n') |
|
156 if not fromformfilter: |
|
157 self.w(u'</div>\n') |
|
158 |
|
159 |
|
160 def show_hide_actions(self, divid, currentlydisplayed=False): |
|
161 showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:] |
|
162 for what in ('Form', 'Show', 'Hide', 'Actions')) |
|
163 showhide = 'javascript:' + showhide |
|
164 showlabel = self.req._('show filter form') |
|
165 hidelabel = self.req._('hide filter form') |
|
166 if currentlydisplayed: |
|
167 return [(showhide, showlabel, 'hidden', '%sShow' % divid), |
|
168 (showhide, hidelabel, None, '%sHide' % divid)] |
|
169 return [(showhide, showlabel, None, '%sShow' % divid), |
|
170 (showhide, hidelabel, 'hidden', '%sHide' % divid)] |
|
171 |
|
172 def render_actions(self, divid, actions): |
|
173 box = MenuWidget('', 'tableActionsBox', _class='', islist=False) |
|
174 label = '<img src="%s" alt="%s"/>' % ( |
|
175 self.req.datadir_url + 'liveclipboard-icon.png', |
|
176 html_escape(self.req._('action(s) on this selection'))) |
|
177 menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox', |
|
178 ident='%sActions' % divid) |
|
179 box.append(menu) |
|
180 for url, label, klass, ident in actions: |
|
181 menu.append(BoxLink(url, label, klass, ident=ident, escape=True)) |
|
182 box.render(w=self.w) |
|
183 self.w(u'<div class="clear"/>') |
|
184 |
|
185 def get_columns(self, rqlstdescr, displaycols, headers, subvid, cellvids, |
|
186 cellattrs, mainindex): |
|
187 columns = [] |
|
188 for colindex, attr in enumerate(rqlstdescr): |
|
189 if colindex not in displaycols: |
|
190 continue |
|
191 # compute column header |
|
192 if headers is not None: |
|
193 label = headers[displaycols.index(colindex)] |
|
194 elif colindex == 0 or attr == 'Any': # find a better label |
|
195 label = ','.join(display_name(self.req, et) |
|
196 for et in self.rset.column_types(colindex)) |
|
197 else: |
|
198 label = display_name(self.req, attr) |
|
199 if colindex == mainindex: |
|
200 label += ' (%s)' % self.rset.rowcount |
|
201 column = TableColumn(label, colindex) |
|
202 coltype = self.rset.description[0][colindex] |
|
203 # compute column cell view (if coltype is None, it's a left outer |
|
204 # join, use the default non final subvid) |
|
205 if cellvids and colindex in cellvids: |
|
206 column.append_renderer(cellvids[colindex], colindex) |
|
207 elif coltype is not None and self.schema.eschema(coltype).is_final(): |
|
208 column.append_renderer(self.finalview, colindex) |
|
209 else: |
|
210 column.append_renderer(subvid or 'incontext', colindex) |
|
211 |
|
212 |
|
213 if cellattrs and colindex in cellattrs: |
|
214 for name, value in cellattrs[colindex].iteritems(): |
|
215 column.add_attr(name,value) |
|
216 # add column |
|
217 columns.append(column) |
|
218 return columns |
|
219 |
|
220 |
|
221 def render(self, cellvid, row, col, w): |
|
222 self.view('cell', self.rset, row=row, col=col, cellvid=cellvid, w=w) |
|
223 |
|
224 def get_rows(self): |
|
225 return self.rset |
|
226 |
|
227 @htmlescape |
|
228 @jsonize |
|
229 @limitsize(10) |
|
230 def sortvalue(self, row, col): |
|
231 # XXX it might be interesting to try to limit value's |
|
232 # length as much as possible (e.g. by returning the 10 |
|
233 # first characters of a string) |
|
234 val = self.rset[row][col] |
|
235 if val is None: |
|
236 return u'' |
|
237 etype = self.rset.description[row][col] |
|
238 if self.schema.eschema(etype).is_final(): |
|
239 entity, rtype = self.rset.related_entity(row, col) |
|
240 if entity is None: |
|
241 return val # remove_html_tags() ? |
|
242 return entity.sortvalue(rtype) |
|
243 entity = self.rset.get_entity(row, col) |
|
244 return entity.sortvalue() |
|
245 |
|
246 class EditableTableView(TableView): |
|
247 id = 'editable-table' |
|
248 finalview = 'editable-final' |
|
249 title = _('editable-table') |
|
250 |
|
251 |
|
252 class CellView(EntityView): |
|
253 __selectors__ = (anyrset_selector, accept_rset_selector) |
|
254 |
|
255 id = 'cell' |
|
256 accepts = ('Any',) |
|
257 |
|
258 def cell_call(self, row, col, cellvid=None): |
|
259 """ |
|
260 :param row, col: indexes locating the cell value in view's result set |
|
261 :param cellvid: cell view (defaults to 'outofcontext') |
|
262 """ |
|
263 etype, val = self.rset.description[row][col], self.rset[row][col] |
|
264 if val is not None and not self.schema.eschema(etype).is_final(): |
|
265 e = self.rset.get_entity(row, col) |
|
266 e.view(cellvid or 'outofcontext', w=self.w) |
|
267 elif val is None: |
|
268 # This is usually caused by a left outer join and in that case, |
|
269 # regular views will most certainly fail if they don't have |
|
270 # a real eid |
|
271 self.wview('final', self.rset, row=row, col=col) |
|
272 else: |
|
273 self.wview(cellvid or 'final', self.rset, 'null', row=row, col=col) |
|
274 |
|
275 |
|
276 class InitialTableView(TableView): |
|
277 """same display as table view but consider two rql queries : |
|
278 |
|
279 * the default query (ie `rql` form parameter), which is only used to select |
|
280 this view and to build the filter form. This query should have the same |
|
281 structure as the actual without actual restriction (but link to |
|
282 restriction variables) and usually with a limit for efficiency (limit set |
|
283 to 2 is advised) |
|
284 |
|
285 * the actual query (`actualrql` form parameter) whose results will be |
|
286 displayed with default restrictions set |
|
287 """ |
|
288 id = 'initialtable' |
|
289 __selectors__ = anyrset_selector, req_form_params_selector |
|
290 form_params = ('actualrql',) |
|
291 # should not be displayed in possible view since it expects some specific |
|
292 # parameters |
|
293 title = None |
|
294 |
|
295 def call(self, title=None, subvid=None, headers=None, |
|
296 displaycols=None, displayactions=None): |
|
297 """Dumps a table displaying a composite query""" |
|
298 actrql = self.req.form['actualrql'] |
|
299 self.ensure_ro_rql(actrql) |
|
300 displaycols = self.displaycols(displaycols) |
|
301 if displayactions is None and 'displayactions' in self.req.form: |
|
302 displayactions = True |
|
303 self.w(u'<div class="section">') |
|
304 if not title and 'title' in self.req.form: |
|
305 # pop title so it's not displayed by the table view as well |
|
306 title = self.req.form.pop('title') |
|
307 if title: |
|
308 self.w(u'<h2>%s</h2>\n' % title) |
|
309 mainindex = self.main_var_index() |
|
310 if mainindex is not None: |
|
311 rqlst = self.rset.syntax_tree() |
|
312 # union not yet supported |
|
313 if len(rqlst.children) == 1: |
|
314 rqlst.save_state() |
|
315 mainvar, baserql = prepare_facets_rqlst(rqlst, self.rset.args) |
|
316 facets = list(self.vreg.possible_vobjects('facets', self.req, self.rset, |
|
317 context='tablefilter', |
|
318 filtered_variable=mainvar)) |
|
319 |
|
320 if facets: |
|
321 divid = self.req.form.get('divid', 'filteredTable') |
|
322 self.generate_form(divid, baserql, facets, |
|
323 vidargs={'displaycols': displaycols, |
|
324 'displayactions': displayactions, |
|
325 'displayfilter': True}) |
|
326 actions = self.show_hide_actions(divid, False) |
|
327 rqlst.recover() |
|
328 if not subvid and 'subvid' in self.req.form: |
|
329 subvid = self.req.form.pop('subvid') |
|
330 self.view('table', self.req.execute(actrql), |
|
331 'noresult', w=self.w, displayfilter=False, subvid=subvid, |
|
332 displayactions=displayactions, displaycols=displaycols, |
|
333 actions=actions, headers=headers) |
|
334 self.w(u'</div>\n') |
|
335 |
|
336 |
|
337 class EditableInitiableTableView(InitialTableView): |
|
338 id = 'editable-initialtable' |
|
339 finalview = 'editable-final' |
|
340 |