--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/tableview.py Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,340 @@
+"""generic table view, including filtering abilities
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from simplejson import dumps
+
+from logilab.mtconverter import html_escape
+
+from cubicweb.common.utils import make_uid
+from cubicweb.common.uilib import toggle_action, limitsize, jsonize, htmlescape
+from cubicweb.common.view import EntityView, AnyRsetView
+from cubicweb.common.selectors import (anyrset_selector, req_form_params_selector,
+ accept_rset_selector)
+from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
+ PopupBoxMenu, BoxLink)
+from cubicweb.web.facet import prepare_facets_rqlst, filter_hiddens
+
+class TableView(AnyRsetView):
+ id = 'table'
+ title = _('table')
+ finalview = 'final'
+
+ def generate_form(self, divid, baserql, facets, hidden=True, vidargs={}):
+ """display a form to filter table's content. This should only
+ occurs when a context eid is given
+ """
+ self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.formfilter.js'))
+ # drop False / None values from vidargs
+ vidargs = dict((k, v) for k, v in vidargs.iteritems() if v)
+ self.w(u'<form method="post" cubicweb:facetargs="%s" action="">' %
+ html_escape(dumps([divid, 'table', False, vidargs])))
+ self.w(u'<fieldset id="%sForm" class="%s">' % (divid, hidden and 'hidden' or ''))
+ self.w(u'<input type="hidden" name="divid" value="%s" />' % divid)
+ filter_hiddens(self.w, facets=','.join(facet.id for facet in facets), baserql=baserql)
+ self.w(u'<table class="filter">\n')
+ self.w(u'<tr>\n')
+ for facet in facets:
+ wdg = facet.get_widget()
+ print 'FACT WIDGET', wdg
+ if wdg is not None:
+ self.w(u'<td>')
+ wdg.render(w=self.w)
+ self.w(u'</td>\n')
+ self.w(u'</tr>\n')
+ self.w(u'</table>\n')
+ self.w(u'</fieldset>\n')
+ self.w(u'</form>\n')
+
+ def main_var_index(self):
+ """returns the index of the first non-attribute variable among the RQL
+ selected variables
+ """
+ eschema = self.vreg.schema.eschema
+ for i, etype in enumerate(self.rset.description[0]):
+ try:
+ if not eschema(etype).is_final():
+ return i
+ except KeyError: # XXX possible?
+ continue
+ return None
+
+ def displaycols(self, displaycols):
+ if displaycols is None:
+ if 'displaycols' in self.req.form:
+ displaycols = [int(idx) for idx in self.req.form['displaycols']]
+ else:
+ displaycols = range(len(self.rset.syntax_tree().children[0].selection))
+ return displaycols
+
+ def call(self, title=None, subvid=None, displayfilter=None, headers=None,
+ displaycols=None, displayactions=None, actions=(),
+ cellvids=None, cellattrs=None):
+ """Dumps a table displaying a composite query
+
+ :param title: title added before table
+ :param subvid: cell view
+ :param displayfilter: filter that selects rows to display
+ :param headers: columns' titles
+ """
+ rset = self.rset
+ req = self.req
+ req.add_js('jquery.tablesorter.js')
+ req.add_css('cubicweb.tablesorter.css')
+ rqlst = rset.syntax_tree()
+ # get rql description first since the filter form may remove some
+ # necessary information
+ rqlstdescr = rqlst.get_description()[0] # XXX missing Union support
+ mainindex = self.main_var_index()
+ hidden = True
+ if not subvid and 'subvid' in req.form:
+ subvid = req.form.pop('subvid')
+ divid = req.form.get('divid') or 'rs%s' % make_uid(id(rset))
+ actions = list(actions)
+ if mainindex is None:
+ displayfilter, displayactions = False, False
+ else:
+ if displayfilter is None and 'displayfilter' in req.form:
+ displayfilter = True
+ if req.form['displayfilter'] == 'shown':
+ hidden = False
+ if displayactions is None and 'displayactions' in req.form:
+ displayactions = True
+ displaycols = self.displaycols(displaycols)
+ fromformfilter = 'fromformfilter' in req.form
+ # if fromformfilter is true, this is an ajax call and we only want to
+ # replace the inner div, so don't regenerate everything under the if
+ # below
+ if not fromformfilter:
+ div_class = 'section'
+ self.w(u'<div class="%s">' % div_class)
+ if not title and 'title' in req.form:
+ title = req.form['title']
+ if title:
+ self.w(u'<h2 class="tableTitle">%s</h2>\n' % title)
+ if displayfilter:
+ rqslt.save_state()
+ try:
+ mainvar, baserql = prepare_facets_rqlst(rqlst, rset.args)
+ except NotImplementedError:
+ # UNION query
+ facets = None
+ else:
+ facets = list(self.vreg.possible_vobjects('facets', req, rset,
+ context='tablefilter',
+ filtered_variable=mainvar))
+ self.generate_form(divid, baserql, facets, hidden,
+ vidargs={'displaycols': displaycols,
+ 'displayfilter': displayfilter,
+ 'displayactions': displayactions})
+ actions += self.show_hide_actions(divid, not hidden)
+ rqlst.recover()
+ elif displayfilter:
+ actions += self.show_hide_actions(divid, True)
+ self.w(u'<div id="%s"' % divid)
+ if displayactions:
+ for action in self.vreg.possible_actions(req, self.rset).get('mainactions', ()):
+ actions.append( (action.url(), req._(action.title), action.html_class(), None) )
+ self.w(u' cubicweb:displayactions="1">') # close <div tag
+ else:
+ self.w(u'>') # close <div tag
+ # render actions menu
+ if actions:
+ self.render_actions(divid, actions)
+ # render table
+ table = TableWidget(self)
+ for column in self.get_columns(rqlstdescr, displaycols, headers, subvid,
+ cellvids, cellattrs, mainindex):
+ table.append_column(column)
+ table.render(self.w)
+ self.w(u'</div>\n')
+ if not fromformfilter:
+ self.w(u'</div>\n')
+
+
+ def show_hide_actions(self, divid, currentlydisplayed=False):
+ showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
+ for what in ('Form', 'Show', 'Hide', 'Actions'))
+ showhide = 'javascript:' + showhide
+ showlabel = self.req._('show filter form')
+ hidelabel = self.req._('hide filter form')
+ if currentlydisplayed:
+ return [(showhide, showlabel, 'hidden', '%sShow' % divid),
+ (showhide, hidelabel, None, '%sHide' % divid)]
+ return [(showhide, showlabel, None, '%sShow' % divid),
+ (showhide, hidelabel, 'hidden', '%sHide' % divid)]
+
+ def render_actions(self, divid, actions):
+ box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
+ label = '<img src="%s" alt="%s"/>' % (
+ self.req.datadir_url + 'liveclipboard-icon.png',
+ html_escape(self.req._('action(s) on this selection')))
+ menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
+ ident='%sActions' % divid)
+ box.append(menu)
+ for url, label, klass, ident in actions:
+ menu.append(BoxLink(url, label, klass, ident=ident, escape=True))
+ box.render(w=self.w)
+ self.w(u'<div class="clear"/>')
+
+ def get_columns(self, rqlstdescr, displaycols, headers, subvid, cellvids,
+ cellattrs, mainindex):
+ columns = []
+ for colindex, attr in enumerate(rqlstdescr):
+ if colindex not in displaycols:
+ continue
+ # compute column header
+ if headers is not None:
+ label = headers[displaycols.index(colindex)]
+ elif colindex == 0 or attr == 'Any': # find a better label
+ label = ','.join(display_name(self.req, et)
+ for et in self.rset.column_types(colindex))
+ else:
+ label = display_name(self.req, attr)
+ if colindex == mainindex:
+ label += ' (%s)' % self.rset.rowcount
+ column = TableColumn(label, colindex)
+ coltype = self.rset.description[0][colindex]
+ # compute column cell view (if coltype is None, it's a left outer
+ # join, use the default non final subvid)
+ if cellvids and colindex in cellvids:
+ column.append_renderer(cellvids[colindex], colindex)
+ elif coltype is not None and self.schema.eschema(coltype).is_final():
+ column.append_renderer(self.finalview, colindex)
+ else:
+ column.append_renderer(subvid or 'incontext', colindex)
+
+
+ if cellattrs and colindex in cellattrs:
+ for name, value in cellattrs[colindex].iteritems():
+ column.add_attr(name,value)
+ # add column
+ columns.append(column)
+ return columns
+
+
+ def render(self, cellvid, row, col, w):
+ self.view('cell', self.rset, row=row, col=col, cellvid=cellvid, w=w)
+
+ def get_rows(self):
+ return self.rset
+
+ @htmlescape
+ @jsonize
+ @limitsize(10)
+ def sortvalue(self, row, col):
+ # XXX it might be interesting to try to limit value's
+ # length as much as possible (e.g. by returning the 10
+ # first characters of a string)
+ val = self.rset[row][col]
+ if val is None:
+ return u''
+ etype = self.rset.description[row][col]
+ if self.schema.eschema(etype).is_final():
+ entity, rtype = self.rset.related_entity(row, col)
+ if entity is None:
+ return val # remove_html_tags() ?
+ return entity.sortvalue(rtype)
+ entity = self.rset.get_entity(row, col)
+ return entity.sortvalue()
+
+class EditableTableView(TableView):
+ id = 'editable-table'
+ finalview = 'editable-final'
+ title = _('editable-table')
+
+
+class CellView(EntityView):
+ __selectors__ = (anyrset_selector, accept_rset_selector)
+
+ id = 'cell'
+ accepts = ('Any',)
+
+ def cell_call(self, row, col, cellvid=None):
+ """
+ :param row, col: indexes locating the cell value in view's result set
+ :param cellvid: cell view (defaults to 'outofcontext')
+ """
+ etype, val = self.rset.description[row][col], self.rset[row][col]
+ if val is not None and not self.schema.eschema(etype).is_final():
+ e = self.rset.get_entity(row, col)
+ e.view(cellvid or 'outofcontext', w=self.w)
+ elif val is None:
+ # This is usually caused by a left outer join and in that case,
+ # regular views will most certainly fail if they don't have
+ # a real eid
+ self.wview('final', self.rset, row=row, col=col)
+ else:
+ self.wview(cellvid or 'final', self.rset, 'null', row=row, col=col)
+
+
+class InitialTableView(TableView):
+ """same display as table view but consider two rql queries :
+
+ * the default query (ie `rql` form parameter), which is only used to select
+ this view and to build the filter form. This query should have the same
+ structure as the actual without actual restriction (but link to
+ restriction variables) and usually with a limit for efficiency (limit set
+ to 2 is advised)
+
+ * the actual query (`actualrql` form parameter) whose results will be
+ displayed with default restrictions set
+ """
+ id = 'initialtable'
+ __selectors__ = anyrset_selector, req_form_params_selector
+ form_params = ('actualrql',)
+ # should not be displayed in possible view since it expects some specific
+ # parameters
+ title = None
+
+ def call(self, title=None, subvid=None, headers=None,
+ displaycols=None, displayactions=None):
+ """Dumps a table displaying a composite query"""
+ actrql = self.req.form['actualrql']
+ self.ensure_ro_rql(actrql)
+ displaycols = self.displaycols(displaycols)
+ if displayactions is None and 'displayactions' in self.req.form:
+ displayactions = True
+ self.w(u'<div class="section">')
+ if not title and 'title' in self.req.form:
+ # pop title so it's not displayed by the table view as well
+ title = self.req.form.pop('title')
+ if title:
+ self.w(u'<h2>%s</h2>\n' % title)
+ mainindex = self.main_var_index()
+ if mainindex is not None:
+ rqlst = self.rset.syntax_tree()
+ # union not yet supported
+ if len(rqlst.children) == 1:
+ rqlst.save_state()
+ mainvar, baserql = prepare_facets_rqlst(rqlst, self.rset.args)
+ facets = list(self.vreg.possible_vobjects('facets', self.req, self.rset,
+ context='tablefilter',
+ filtered_variable=mainvar))
+
+ if facets:
+ divid = self.req.form.get('divid', 'filteredTable')
+ self.generate_form(divid, baserql, facets,
+ vidargs={'displaycols': displaycols,
+ 'displayactions': displayactions,
+ 'displayfilter': True})
+ actions = self.show_hide_actions(divid, False)
+ rqlst.recover()
+ if not subvid and 'subvid' in self.req.form:
+ subvid = self.req.form.pop('subvid')
+ self.view('table', self.req.execute(actrql),
+ 'noresult', w=self.w, displayfilter=False, subvid=subvid,
+ displayactions=displayactions, displaycols=displaycols,
+ actions=actions, headers=headers)
+ self.w(u'</div>\n')
+
+
+class EditableInitiableTableView(InitialTableView):
+ id = 'editable-initialtable'
+ finalview = 'editable-final'
+