web/views/tableview.py
changeset 0 b97547f5f1fa
child 16 a70ece4d9d1a
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     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