# HG changeset patch # User Denis Laxalde # Date 1378723405 -7200 # Node ID 2dae5bf5ea68e8d543762bdf718f249db613fb3c # Parent 212869484c653c8a137c4f2b35e5d4ab678fb379 [ReST] Implement a rql-table reST directive. Closes #3252856 allowing to call table or derivated view specify headers / cellvids. Also, rql may be split accross several lines which greatly improve readability. diff -r 212869484c65 -r 2dae5bf5ea68 ext/rest.py --- a/ext/rest.py Wed Oct 16 11:57:47 2013 +0200 +++ b/ext/rest.py Mon Sep 09 12:43:25 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -29,6 +29,8 @@ * `sourcecode` (if pygments is installed), source code colorization +* `rql-table`, create a table from a RQL query + """ __docformat__ = "restructuredtext en" @@ -40,7 +42,7 @@ from docutils import statemachine, nodes, utils, io from docutils.core import Publisher -from docutils.parsers.rst import Parser, states, directives +from docutils.parsers.rst import Parser, states, directives, Directive from docutils.parsers.rst.roles import register_canonical_role, set_classes from logilab.mtconverter import ESC_UCAR_TABLE, ESC_CAR_TABLE, xml_escape @@ -251,6 +253,76 @@ winclude_directive.options = {'literal': directives.flag, 'encoding': directives.encoding} +class RQLTableDirective(Directive): + """rql-table directive + + Example: + + .. rql-table:: + :vid: mytable + :headers: , , progress + :colvids: 2=progress + + Any X,U,X WHERE X is Project, X url U + + All fields but the RQL string are optionnal. The ``:headers:`` option can + contain empty column names. + """ + + required_arguments = 0 + optional_arguments = 0 + has_content= True + final_argument_whitespace = True + option_spec = {'vid': directives.unchanged, + 'headers': directives.unchanged, + 'colvids': directives.unchanged} + + def run(self): + errid = "rql-table directive" + self.assert_has_content() + if self.arguments: + raise self.warning('%s does not accept arguments' % errid) + rql = ' '.join([l.strip() for l in self.content]) + _cw = self.state.document.settings.context._cw + _cw.ensure_ro_rql(rql) + try: + rset = _cw.execute(rql) + except Exception as exc: + raise self.error("fail to execute RQL query in %s: %r" % + (errid, exc)) + if not rset: + raise self.warning("empty result set") + vid = self.options.get('vid', 'table') + try: + view = _cw.vreg['views'].select(vid, _cw, rset=rset) + except Exception as exc: + raise self.error("fail to select '%s' view in %s: %r" % + (vid, errid, exc)) + headers = None + if 'headers' in self.options: + headers = [h.strip() for h in self.options['headers'].split(',')] + while headers.count(''): + headers[headers.index('')] = None + if len(headers) != len(rset[0]): + raise self.error("the number of 'headers' does not match the " + "number of columns in %s" % errid) + cellvids = None + if 'colvids' in self.options: + cellvids = {} + for f in self.options['colvids'].split(','): + try: + idx, vid = f.strip().split('=') + except ValueError: + raise self.error("malformatted 'colvids' option in %s" % + errid) + cellvids[int(idx.strip())] = vid.strip() + try: + content = view.render(headers=headers, cellvids=cellvids) + except Exception as exc: + raise self.error("Error rendering %s (%s)" % (errid, exc)) + return [nodes.raw('', content, format='html')] + + try: from pygments import highlight from pygments.lexers import get_lexer_by_name @@ -385,3 +457,4 @@ directives.register_directive('winclude', winclude_directive) if pygments_directive is not None: directives.register_directive('sourcecode', pygments_directive) + directives.register_directive('rql-table', RQLTableDirective) diff -r 212869484c65 -r 2dae5bf5ea68 ext/test/data/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/test/data/views.py Mon Sep 09 12:43:25 2013 +0200 @@ -0,0 +1,24 @@ +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . + + +from cubicweb.web.views import tableview + +class CustomRsetTableView(tableview.RsetTableView): + __regid__ = 'mytable' + diff -r 212869484c65 -r 2dae5bf5ea68 ext/test/unittest_rest.py --- a/ext/test/unittest_rest.py Wed Oct 16 11:57:47 2013 +0200 +++ b/ext/test/unittest_rest.py Mon Sep 09 12:43:25 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -82,5 +82,133 @@ out = rest_publish(context, ':bookmark:`%s`' % eid) self.assertEqual(out, u'

CWUser_plural

\n') + def test_rqltable_nocontent(self): + context = self.context() + out = rest_publish(context, """.. rql-table::""") + self.assertIn("System Message: ERROR", out) + self.assertIn("Content block expected for the "rql-table" " + "directive; none found" , out) + + def test_rqltable_norset(self): + context = self.context() + rql = "Any X WHERE X is CWUser, X firstname 'franky'" + out = rest_publish( + context, """\ +.. rql-table:: + + %(rql)s""" % {'rql': rql}) + self.assertIn("System Message: WARNING", out) + self.assertIn("empty result set", out) + + def test_rqltable_nooptions(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + out = rest_publish( + self.context(), """\ +.. rql-table:: + + %(rql)s + """ % {'rql': rql}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_vid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytable' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + req = self.request() + view = self.vreg['views'].select(vid, req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + self.assertIn(vid, out[:49]) + + def test_rqltable_badvid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytabel' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + self.assertIn("fail to select '%s' view" % vid, out) + + def test_rqltable_headers(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "prenom", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = headers + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [headers[0], None, headers[2]] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing_edges(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = [" ", "prenom", ""] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [None, headers[1], None] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_colvids(self): + rql = """Any X,S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + colvids = {0: "oneline"} + out = rest_publish( + self.context(), """\ +.. rql-table:: + :colvids: %(colvids)s + + %(rql)s + """ % {'rql': rql, + 'colvids': ', '.join(["%d=%s" % (k, v) + for k, v in colvids.iteritems()]) + }) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.cellvids = colvids + self.assertEqual(view.render(w=None)[49:], out[49:]) + + if __name__ == '__main__': unittest_main()