Browse Source

add filter in page

Daniel Sheffield 1 year ago
parent
commit
1f07ab7c7f
5 changed files with 97 additions and 6 deletions
  1. 1 0
      app/rest/filter-heading.tpl
  2. 10 0
      app/rest/filter-item.tpl
  3. 11 0
      app/rest/filter.tpl
  4. 74 6
      app/rest/pyapi.py
  5. 1 0
      requirements.txt

+ 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
 # 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"?>

+ 1 - 0
requirements.txt

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