|
@@ -3,12 +3,23 @@
|
|
# All rights reserved
|
|
# All rights reserved
|
|
#
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
-from bottle import route, request, run, response, abort
|
|
|
|
|
|
+from typing import Iterable
|
|
|
|
+from bottle import (
|
|
|
|
+ route,
|
|
|
|
+ request,
|
|
|
|
+ run,
|
|
|
|
+ response,
|
|
|
|
+ abort,
|
|
|
|
+ DictProperty,
|
|
|
|
+ redirect,
|
|
|
|
+ template,
|
|
|
|
+)
|
|
from psycopg import connect
|
|
from psycopg import connect
|
|
from psycopg.sql import SQL, Literal
|
|
from psycopg.sql import SQL, Literal
|
|
import os
|
|
import os
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.pyplot as plt
|
|
import seaborn as sns
|
|
import seaborn as sns
|
|
|
|
+from urllib.parse import urlencode
|
|
from ..activities.Plot import (
|
|
from ..activities.Plot import (
|
|
get_data,
|
|
get_data,
|
|
)
|
|
)
|
|
@@ -35,15 +46,58 @@ conn = connect(f"{host} {db} {user} {password}")
|
|
sns.set_theme()
|
|
sns.set_theme()
|
|
|
|
|
|
from io import StringIO
|
|
from io import StringIO
|
|
|
|
+
|
|
|
|
+def get_filter(query: DictProperty, allow: Iterable[str] = None):
|
|
|
|
+ return {
|
|
|
|
+ k: get_include_exclude(
|
|
|
|
+ query[k] if k in query else None
|
|
|
|
+ ) for k in sorted(set(query.keys()) | set(allow)) if allow is None or k in allow
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+def get_query_param(include, exclude):
|
|
|
|
+ return '!'.join([
|
|
|
|
+ '|'.join(sorted(include)),
|
|
|
|
+ *([ '|'.join(sorted(exclude)) ] if exclude else [ ]),
|
|
|
|
+ ])
|
|
|
|
+
|
|
|
|
+def get_query(**params):
|
|
|
|
+ return '&'.join([
|
|
|
|
+ urlencode([ (k, get_query_param(*params[k])) for k in sorted(params) ])
|
|
|
|
+ ])
|
|
|
|
+
|
|
|
|
+def normalize_query(query: DictProperty, allow: Iterable[str] = None):
|
|
|
|
+ return get_query(**get_filter(query, allow=allow))
|
|
|
|
+
|
|
|
|
+def get_form(action, method, **data):
|
|
|
|
+ return template(
|
|
|
|
+ 'app/rest/filter.tpl',
|
|
|
|
+ action=action,
|
|
|
|
+ method=method,
|
|
|
|
+ header=[ template('app/rest/filter-heading', fname=k) for k in sorted(data) ],
|
|
|
|
+ items=[ template(
|
|
|
|
+ 'app/rest/filter-item.tpl',
|
|
|
|
+ fname=k,
|
|
|
|
+ fvalue=get_query_param(*data[k]),
|
|
|
|
+ size=36,
|
|
|
|
+ ) for k in sorted(data) ]
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+
|
|
@route('/grocery/trend')
|
|
@route('/grocery/trend')
|
|
def trend():
|
|
def trend():
|
|
|
|
+ _, _, path, *_ = request.urlparts
|
|
|
|
+ normalized = normalize_query(request.query, allow=PARAMS)
|
|
|
|
+
|
|
|
|
+ if request.query_string != normalized:
|
|
|
|
+ return redirect(f'{path}?{normalized}')
|
|
try:
|
|
try:
|
|
with conn.cursor() as cur:
|
|
with conn.cursor() as cur:
|
|
query_manager = QueryManager(cur, display_mapper)
|
|
query_manager = QueryManager(cur, display_mapper)
|
|
- fields = { k: request.query[k] for k in request.query.keys() if k in PARAMS }
|
|
|
|
- unit = fields['unit'] = fields['unit'] if 'unit' in fields else None or 'kg'
|
|
|
|
- if unit not in ALL_UNITS:
|
|
|
|
|
|
+ fields = { k: request.query[k] or None for k in request.query.keys() if k in PARAMS }
|
|
|
|
+ unit = fields['unit'] = fields['unit'] or 'kg' if 'unit' in fields else 'kg'
|
|
|
|
+ if unit and unit not in ALL_UNITS:
|
|
raise abort(400, f"Unsupported unit {unit}")
|
|
raise abort(400, f"Unsupported unit {unit}")
|
|
|
|
+
|
|
data = get_data(query_manager, **fields)
|
|
data = get_data(query_manager, **fields)
|
|
if data.empty:
|
|
if data.empty:
|
|
raise abort(404, f"No data for {fields}")
|
|
raise abort(404, f"No data for {fields}")
|
|
@@ -56,8 +110,22 @@ def trend():
|
|
finally:
|
|
finally:
|
|
conn.commit()
|
|
conn.commit()
|
|
|
|
|
|
- response.content_type = 'image/svg+xml; charset=utf-8'
|
|
|
|
- return f.getvalue()
|
|
|
|
|
|
+ return 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))}
|
|
|
|
+{f.getvalue()}
|
|
|
|
+ </body>
|
|
|
|
+</html>
|
|
|
|
+"""
|
|
|
|
|
|
heading = """<?xml version="1.0" encoding="UTF-8"?>
|
|
heading = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<?xml-stylesheet type="text/xsl" href="/grocery/style/table"?>
|
|
<?xml-stylesheet type="text/xsl" href="/grocery/style/table"?>
|