[ReST] Implement a rql-table reST directive. Closes #3252856
authorDenis Laxalde <denis.laxalde@logilab.fr>
Mon, 09 Sep 2013 12:43:25 +0200
changeset 9322 2dae5bf5ea68
parent 9321 212869484c65
child 9340 b1e933b0e850
[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.
ext/rest.py
ext/test/data/views.py
ext/test/unittest_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)
--- /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 <http://www.gnu.org/licenses/>.
+
+
+from cubicweb.web.views import tableview
+
+class CustomRsetTableView(tableview.RsetTableView):
+    __regid__ = 'mytable'
+
--- 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'<p><h1>CWUser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\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 &quot;rql-table&quot; "
+                      "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()