|
@@ -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.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)
|