Ver Fonte

use rollup statement for tables and move xslt/xml to templates

Daniel Sheffield há 1 ano atrás
pai
commit
cd50be93c7
4 ficheiros alterados com 182 adições e 136 exclusões
  1. 102 136
      app/rest/pyapi.py
  2. 63 0
      app/rest/query-to-xml-xslt.tpl
  3. 3 0
      app/rest/query-to-xml.tpl
  4. 14 0
      app/rest/trend.tpl

+ 102 - 136
app/rest/pyapi.py

@@ -51,6 +51,34 @@ if not password.split('=',1)[1]:
 conn = connect(f"{host} {db} {user} {password}")
 sns.set_theme()
 
+def get_product_rollup_statement(filters, having=None):
+    where = [ get_where_include_exclude(
+        k[0], "name", include, exclude
+    ) for k, (include, exclude) in filters.items() ]
+    return SQL('\n').join([
+        SQL("""
+SELECT
+  count(DISTINCT p.id) AS "Products",
+  count(DISTINCT c.id) AS "Categories",
+  count(DISTINCT g.id) AS "Groups",
+  p.name AS "Product",
+  c.name AS "Category",
+  g.name AS "Group"
+FROM products p
+JOIN categories c ON p.category_id = c.id
+JOIN groups g ON c.group_id = g.id
+"""),
+        SQL("""
+WHERE {where}
+""").format(where=SQL("\nAND").join(where)) if where else SQL(''),
+        SQL("""
+GROUP BY ROLLUP (g.name, c.name, p.name)
+"""),
+        SQL("""
+HAVING {having}
+""").format(having=having) if having else SQL('')
+    ])
+
 def get_filter(query: DictProperty, allow: Iterable[str] = None):
     return {
         k: get_include_exclude(
@@ -153,175 +181,108 @@ def trend_internal(path, query):
             line(pivot, xlabel='Time', ylabel=f'$ / {unit}')
             f = StringIO()
             plt.savefig(f, format='svg')
-            resp = lambda: f"""
-<!DOCTYPE html>
-<html>
-    <head>
-        <title>Trend</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1"/>
-        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous"/>
-        <link rel="stylesheet" href="https://shandan.one/css/grids-responsive-min.css"/>
-        <link rel="stylesheet" href="https://shandan.one/css/responsive-visibility-collapse.css"/>
-    </head>
-    <body align="center">
-{get_form(path, 'get', get_filter(request.query, allow=PARAMS), data)}
-{f.getvalue()}
-    </body>
-</html>
-"""
+            form = get_form(path, 'get', get_filter(request.query, allow=PARAMS), data)
+            resp = lambda: template("app/rest/trend", form=form, svg=f.getvalue())
+
     except HTTPError as e:
         resp = lambda exception=e: exception
+
     finally:
         conn.commit()
      
     yield from iter(resp, lambda started=time(): time() - started > 600)
 
-heading = """<?xml version="1.0" encoding="UTF-8"?>
-<?xml-stylesheet type="text/xsl" href="/grocery/style/table"?>
-"""
-
-style = """
-<xsl:stylesheet version="1.0"
-    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
-    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
-    xmlns="http://www.w3.org/1999/xhtml"
->
-
-  <xsl:output method="xml"
-      doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
-      doctype-public="-//W3C/DTD XHTML 1.0 Strict//EN"
-      indent="yes"/>
-
-  <xsl:template match="/*">
-    <xsl:variable name="schema" select="//xsd:schema"/>
-    <xsl:variable name="tabletypename"
-                  select="$schema/xsd:element[@name=name(current())]/@type"/>
-    <xsl:variable name="rowtypename"
-                  select="$schema/xsd:complexType[@name=$tabletypename]/xsd:sequence/xsd:element[@name='row']/@type"/>
-    <xsl:variable name="tablename" select="$schema/xsd:complexType[@name=$rowtypename]/xsd:sequence/xsd:element[1]/@name"/>
-
-    <html>
-      <head>
-        <title>
-        <xsl:choose>
-          <xsl:when test="$tablename = 'Group'">Groups</xsl:when>
-          <xsl:when test="$tablename = 'Category'">Categories</xsl:when>
-          <xsl:when test="$tablename = 'Product'">Products</xsl:when>
-          <xsl:otherwise></xsl:otherwise>
-        </xsl:choose>
-        </title>
-        <meta name="viewport" content="width=device-width, initial-scale=1"/>
-        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous"/>
-        <link rel="stylesheet" href="https://shandan.one/css/grids-responsive-min.css"/>
-        <link rel="stylesheet" href="https://shandan.one/css/responsive-visibility-collapse.css"/>
-      </head>
-      <body>
-        <table class="pure-table pure-table-bordered pure-table-striped">
-          <thead>
-          <tr>
-            <xsl:for-each select="$schema/xsd:complexType[@name=$rowtypename]/xsd:sequence/xsd:element/@name">
-              <th><xsl:value-of select="."/></th>
-            </xsl:for-each>
-          </tr>
-          </thead>
-
-          <xsl:for-each select="row">
-            <tr>
-              <xsl:for-each select="*">
-                <td><xsl:value-of select="."/></td>
-              </xsl:for-each>
-            </tr>
-          </xsl:for-each>
-        </table>
-      </body>
-    </html>
-  </xsl:template>
-
-</xsl:stylesheet>
-"""
-@route('/grocery/style/table')
+@route('/grocery/xslt')
 def table():
+    title = request.query['title'] if 'title' in request.query.keys() else None
+    foot = request.query['foot'] == 'True' and "last()" if 'foot' in request.query.keys() else "-1"
     response.content_type = 'application/xhtml+xml; charset=utf-8'
-    return style
+    return template("app/rest/query-to-xml-xslt", title=title, foot=foot)
 
 @route('/grocery/groups')
 def groups():
+    filters = get_filter(request.query, allow=('group', 'category', 'product'))
     try:
         with conn.cursor() as cur:
-            xml = cur.execute("""
-SELECT query_to_xml_and_xmlschema('SELECT
-  g.name AS "Group"
-FROM groups g
-ORDER BY 1', false, false, ''::text)
-""").fetchone()[0].splitlines()
+            inner = get_product_rollup_statement(
+                filters,
+                having=SQL("c.name IS NULL")
+            )
+            xml = cur.execute(SQL("""
+SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
+""").format(q=Literal(SQL("""SELECT
+    "Products",
+    "Categories",
+    COALESCE("Group", "Groups"||'') "Group"
+FROM (
+{inner}
+) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
     finally:
         conn.commit()
-    #response.set_header('Access-Control-Allow-Origin', 'https://shandan.one')
-    #response.set_header('Access-Control-Allow-Origin', '*')
-    #response.set_header('Access-Control-Allow-Methods', 'GET')
-    #response.set_header('Access-Control-Allow-Headers', 'text/xml, application/xml, application/xhtml+xml, text/xsl, application/rss+xml, application/atom+xml')
     response.content_type = 'application/xhtml+xml; charset=utf-8'
-    return f"{heading}" + '\n'.join(xml)
+    return template(
+        "app/rest/query-to-xml",
+        title="Groups",
+        foot=True,
+        xml=xml
+    )
 
 @route('/grocery/categories')
 def categories():
-    fields = { 'group': '' }
-    fields.update({
-        k: request.query[k] for k in request.query.keys() if k == 'group'
-    })
+    filters = get_filter(request.query, allow=('group', 'category', 'product'))
     try:
         with conn.cursor() as cur:
-            inner = SQL('\n').join([SQL("""
-SELECT c.name AS "Category",  g.name AS "Group"
-FROM categories c
-JOIN groups g ON c.group_id = g.id
-WHERE
-"""), get_where_include_exclude(
-    "g", "name", *get_include_exclude(fields['group'])
-), SQL("""
-ORDER BY 1, 2
-""")]).as_string(cur)
+            inner = get_product_rollup_statement(
+                filters,
+                having=SQL("p.name IS NULL AND (c.name IS NOT NULL OR g.name IS NULL)")
+            )
             xml = cur.execute(SQL("""
-SELECT query_to_xml_and_xmlschema({inner}, false, false, ''::text)
-""").format(inner=Literal(inner))).fetchone()[0].splitlines()
+SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
+""").format(q=Literal(SQL("""SELECT
+    "Products",
+    COALESCE("Category", "Categories"||'') "Category",
+    COALESCE("Group", "Groups"||'') "Group"
+FROM (
+{inner}
+) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
     finally:
         conn.commit()
     response.content_type = 'application/xhtml+xml; charset=utf-8'
-    return f"{heading}" + '\n'.join(xml)
+    return template(
+        "app/rest/query-to-xml",
+        title="Categories",
+        foot=True,
+        xml=xml
+    )
 
 @route('/grocery/products')
 def products():
-    fields = {
-        'group': '',
-        'category': ''
-    }
-    fields.update({
-        k: request.query[k] for k in request.query.keys() if k in (
-            'group', 'category'
-        )
-    })
+    filters = get_filter(request.query, allow=('group', 'category', 'product'))
     try:
         with conn.cursor() as cur:
-            inner = SQL('\n').join([SQL("""
-SELECT p.name AS "Product", c.name AS "Category", g.name AS "Group"
-FROM products p
-JOIN categories c ON p.category_id = c.id
-JOIN groups g ON c.group_id = g.id
-WHERE
-"""), SQL('\nAND\n').join([get_where_include_exclude(
-    "g", "name", *get_include_exclude(fields['group'])
-), get_where_include_exclude(
-    "c", "name", *get_include_exclude(fields['category'])
-)]), SQL("""
-ORDER BY 1, 2, 3
-""")]).as_string(cur)
+            inner = get_product_rollup_statement(
+                filters,
+                having=SQL("p.name IS NOT NULL OR g.name IS NULL")
+            )
             xml = cur.execute(SQL("""
-SELECT query_to_xml_and_xmlschema({inner}, false, false, ''::text)
-""").format(inner=Literal(inner))).fetchone()[0].splitlines()
+SELECT query_to_xml_and_xmlschema({q}, false, false, ''::text)
+""").format(q=Literal(SQL("""SELECT
+    --"Transactions",
+    COALESCE("Product", "Products"||'') "Product",
+    COALESCE("Category", "Categories"||'') "Category",
+    COALESCE("Group", "Groups"||'') "Group"
+FROM (
+{inner}
+) q""").format(inner=inner).as_string(cur)))).fetchone()[0]
     finally:
         conn.commit()
     response.content_type = 'application/xhtml+xml; charset=utf-8'
-    return f"{heading}" + '\n'.join(xml)
+    return template(
+        "app/rest/query-to-xml",
+        title="Products",
+        foot=True,
+        xml=xml
+    )
 
 @route('/grocery/tags')
 def tags():
@@ -337,10 +298,15 @@ ORDER BY 1 DESC, 2
 """)]).as_string(cur)
             xml = cur.execute(SQL("""
 SELECT query_to_xml_and_xmlschema({inner}, false, false, ''::text)
-""").format(inner=Literal(inner))).fetchone()[0].splitlines()
+""").format(inner=Literal(inner))).fetchone()[0]
     finally:
         conn.commit()
     response.content_type = 'application/xhtml+xml; charset=utf-8'
-    return f"{heading}" + '\n'.join(xml)
+    return template(
+        "app/rest/query-to-xml",
+        title="Tags",
+        foot="false",
+        xml=xml
+    )
 
 run(host='0.0.0.0', port=6772)

+ 63 - 0
app/rest/query-to-xml-xslt.tpl

@@ -0,0 +1,63 @@
+<xsl:stylesheet version="1.0"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns="http://www.w3.org/1999/xhtml"
+>
+
+  <xsl:output method="xml"
+      doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
+      doctype-public="-//W3C/DTD XHTML 1.0 Strict//EN"
+      indent="yes"/>
+
+  <xsl:template match="/*">
+    <xsl:variable name="schema" select="//xsd:schema"/>
+    <xsl:variable name="tabletypename"
+                  select="$schema/xsd:element[@name=name(current())]/@type"/>
+    <xsl:variable name="rowtypename"
+                  select="$schema/xsd:complexType[@name=$tabletypename]/xsd:sequence/xsd:element[@name='row']/@type"/>
+    <xsl:variable name="tablename" select="$schema/xsd:complexType[@name=$rowtypename]/xsd:sequence/xsd:element[1]/@name"/>
+
+    <html>
+      <head>
+        <title>{{ title }}</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1"/>
+        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous"/>
+        <link rel="stylesheet" href="https://shandan.one/css/grids-responsive-min.css"/>
+        <link rel="stylesheet" href="https://shandan.one/css/responsive-visibility-collapse.css"/>
+      </head>
+      <body>
+        <table class="pure-table pure-table-bordered pure-table-striped">
+          <thead>
+          <tr>
+            <xsl:for-each select="$schema/xsd:complexType[@name=$rowtypename]/xsd:sequence/xsd:element/@name">
+              <th><xsl:value-of select="."/></th>
+            </xsl:for-each>
+          </tr>
+          </thead>
+
+          <xsl:for-each select="row">
+            <xsl:choose>
+              <xsl:when test="position() = {{foot}}">
+                <tfoot>
+                  <tr>
+                    <xsl:for-each select="*">
+                      <th><xsl:value-of select="."/></th>
+                    </xsl:for-each>
+                  </tr>
+                </tfoot>
+              </xsl:when>
+              <xsl:otherwise>
+                <tr>
+                  <xsl:for-each select="*">
+                    <td><xsl:value-of select="."/></td>
+                  </xsl:for-each>
+                </tr>
+              </xsl:otherwise>
+            </xsl:choose>
+          </xsl:for-each>
+        </table>
+      </body>
+    </html>
+  </xsl:template>
+
+</xsl:stylesheet>

+ 3 - 0
app/rest/query-to-xml.tpl

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="/grocery/xslt?title={{title}}&amp;foot={{foot}}"?>
+{{!xml}}

+ 14 - 0
app/rest/trend.tpl

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Trend</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1"/>
+        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous"/>
+        <link rel="stylesheet" href="https://shandan.one/css/grids-responsive-min.css"/>
+        <link rel="stylesheet" href="https://shandan.one/css/responsive-visibility-collapse.css"/>
+    </head>
+    <body align="center">
+{{!form}}
+{{!svg}}
+    </body>
+</html>