Daniel Sheffield 1 жил өмнө
parent
commit
1f07ab7c7f

+ 1 - 0
app/rest/filter-heading.tpl

@@ -0,0 +1 @@
+<th>{{fname.title()}}</th>

+ 10 - 0
app/rest/filter-item.tpl

@@ -0,0 +1,10 @@
+<td>
+<input
+  type="text"
+  id={{fname}}"
+  name="{{fname}}"
+  size="{{size}}"
+  pattern="((.*\\|)*(.*))?(!(.*\\|)*(.*))?"
+  title="Must be of the form include!exclude where include and exclude are | separated lists"
+  value="{{fvalue}}" />
+</td>

+ 11 - 0
app/rest/filter.tpl

@@ -0,0 +1,11 @@
+<form id="filter" style="display: inline-block">
+    <table>
+        <tr>
+{{! ''.join( header ) }}
+        </tr>
+        <tr>
+{{! ''.join( items ) }}
+        </tr>
+    </table>
+    <button type="submit" method="{{ method }}" action="{{ action }}">Apply Filter</button>
+</form>

+ 74 - 6
app/rest/pyapi.py

@@ -3,12 +3,23 @@
 # All rights reserved
 #
 # 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.sql import SQL, Literal
 import os
 import matplotlib.pyplot as plt
 import seaborn as sns
+from urllib.parse import urlencode
 from ..activities.Plot import (
     get_data,
 )
@@ -35,15 +46,58 @@ conn = connect(f"{host} {db} {user} {password}")
 sns.set_theme()
 
 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')
 def trend():
+    _, _, path, *_ = request.urlparts
+    normalized = normalize_query(request.query, allow=PARAMS)
+
+    if request.query_string != normalized:
+        return redirect(f'{path}?{normalized}')
     try:
         with conn.cursor() as cur:
             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}")
+
             data = get_data(query_manager, **fields)
             if data.empty:
                 raise abort(404, f"No data for {fields}")
@@ -56,8 +110,22 @@ def trend():
     finally:
         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"?>
 <?xml-stylesheet type="text/xsl" href="/grocery/style/table"?>

+ 1 - 0
requirements.txt

@@ -9,3 +9,4 @@ pandas
 pyyaml
 numpy
 seaborn
+urllib