Browse Source

web UI filter form uses multi-select and specifying URL key multiple times is supported

Daniel Sheffield 1 year ago
parent
commit
d1f4882aac
7 changed files with 99 additions and 57 deletions
  1. 13 4
      app/data/util.py
  2. 9 7
      app/rest/filter-item.tpl
  3. 6 0
      app/rest/filter-option.tpl
  4. 20 22
      app/rest/form.tpl
  5. 50 19
      app/rest/pyapi.py
  6. 0 4
      app/rest/tag-item.tpl
  7. 1 1
      app/rest/trend.tpl

+ 13 - 4
app/data/util.py

@@ -4,7 +4,7 @@
 # All rights reserved
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
-from typing import Tuple, Iterable
+from typing import List, Set, Tuple, Iterable
 from psycopg.sql import (
     Identifier,
     Literal,
@@ -13,11 +13,20 @@ from psycopg.sql import (
 )
 
 
-def get_include_exclude(value):
+def get_include_exclude(value) -> Tuple[List[str], List[str]]:
     value = value or ''
-    include, exclude, *_ = [
+    include, exclude = [], []
+    if isinstance(value, (list, tuple)):
+        for v in value:
+            inc, exc = get_include_exclude(v)
+            include.extend(inc)
+            exclude.extend(exc)
+    else:
+        inc, exc, *_ = [
             *map(lambda x: x.split('|') if x else [], value.split('!')), []
-    ]
+        ]
+        include.extend(inc)
+        exclude.extend(exc)
     return list(set(include)), list(set(exclude))
 
 

+ 9 - 7
app/rest/filter-item.tpl

@@ -1,9 +1,11 @@
 <td>
-<input
-  type="text"
-  id={{fname}}"
-  name="{{fname}}"
-  pattern="((.*\\|)*(.*))?(!(.*\\|)*(.*))?"
-  title="Must be of the form include!exclude where include and exclude are | separated lists"
-  value="{{fvalue}}" />
+  <!--<label for="{{id}}">{{label}}</label>-->
+  <select id="{{id}}" name="{{fname}}" size=10 multiple>
+    <%
+      include('app/rest/filter-option', value=hint, disabled=True)
+      for opt in options:
+        include('app/rest/filter-option', **opt)
+      end
+    %>
+  </select>
 </td>

+ 6 - 0
app/rest/filter-option.tpl

@@ -0,0 +1,6 @@
+<option
+  value="{{value}}"
+  {{"disabled" if get("disabled", False) else ""}}
+  {{"selected" if get("selected", False) else ""}}>
+  {{display if defined("display") else value}}
+</option>

+ 20 - 22
app/rest/form.tpl

@@ -6,7 +6,10 @@
 {{! ''.join( header )}}
             </tr>
             <tr>
-{{! ''.join( items )}}
+{{! ''.join( _include )}}
+            </tr>
+            <tr>
+{{! ''.join( _exclude )}}
             </tr>
         </thead>
         <tbody>       
@@ -28,32 +31,27 @@
             </tr>
             <tr>
                 <td>
-                    <input
-                        type="text"
-                        id="tag"
-                        name="tag"
-                        size="10"
-                        pattern="((.*\\|)*(.*))?(!(.*\\|)*(.*))?"
-                        title="Must be of the form include!exclude where include and exclude are | separated lists"
-                        value="{{tag}}"
-                    />
+                    <select id="tag" name="tag" size=10 multiple>
+                        <%
+                        include('app/rest/filter-option', value="Select", disabled=True)
+                        for opt in tags:
+                            include('app/rest/filter-option', **opt)
+                        end
+                        %>
+                    </select>
                 </td>
                 <td>
-                    <input
-                        type="text"
-                        id="unit"
-                        name="unit"
-                        size="6"
-                        pattern="{{pattern}}"
-                        title="Must be one of: {{units}}"
-                        value="{{unit}}"
-                    />
+                    <select id="unit" name="unit" size=10>
+                        <%
+                        include('app/rest/filter-option', value="Select", disabled=True)
+                        for opt in units:
+                            include('app/rest/filter-option', **opt)
+                        end
+                        %>
+                    </select>
                 </td>
             </tr>
         </thead>
-        <tbody>
-{{! ''.join( tags )}}
-        </tbody>
     </table>
     </div>
 </form>

+ 50 - 19
app/rest/pyapi.py

@@ -15,7 +15,7 @@ from bottle import (
     run,
     response,
     abort,
-    DictProperty,
+    FormsDict,
     redirect,
     template,
     HTTPError,
@@ -80,11 +80,11 @@ ORDER BY "Product", "Category", "Group"
 """).format(having=having) if having else SQL('')
     ])
 
-def get_filter(query: DictProperty, allow: Iterable[str] = None):
+def get_filter(query: FormsDict, allow: Iterable[str] = None):
     return {
-        k: get_include_exclude(
-            (query[k] or 'kg' if k == 'unit' else 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
+        k: list(map(set, get_include_exclude(
+            (query[k] or 'kg' if k == 'unit' else query.getall(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):
@@ -98,7 +98,7 @@ def get_query(**params):
         urlencode([ (k, get_query_param(*params[k])) for k in sorted(params) ])
     ])
 
-def normalize_query(query: DictProperty, allow: Iterable[str] = None):
+def normalize_query(query: FormsDict, allow: Iterable[str] = None):
     return get_query(**get_filter(query, allow=allow))
 
 def get_form(action, method, filter_data, data):
@@ -113,14 +113,10 @@ def get_form(action, method, filter_data, data):
     ]).size().reset_index()[[
         k for k in keys
     ]]
-    tags = set(chain(*data[in_chart]['tags']))
     return template(
         'app/rest/form',
         action=action,
         method=method,
-        unit=get_query_param(*filter_data['unit']),
-        pattern='|'.join(ALL_UNITS),
-        units=', '.join(ALL_UNITS),
         header=[
             template(
                 'app/rest/filter-heading',
@@ -128,24 +124,59 @@ def get_form(action, method, filter_data, data):
                 first=(idx == 0),
             ) for idx, k in enumerate(keys)
         ],
-        items=[ template(
+        _include=[ template(
             'app/rest/filter-item',
+            label="+",
+            hint="Include",
+            id=f"{k}-include",
             fname=k,
-            fvalue=get_query_param(*filter_data[k]),
-            size=36,
+            options=sorted(map(lambda x: {
+                    "selected": x[0],
+                    "value": x[1],
+                }, chain(
+                map(lambda x: (True, x), filter_data[k][0]),
+                map(lambda x: (False, x), set([
+                    x[k] for _, x in group.iterrows()
+                ]) - filter_data[k][0])
+            )), key=lambda x: x["display"] if "display" in x else x["value"]), # include
+        ) for k in keys ],
+        _exclude=[ template(
+            'app/rest/filter-item',
+            label="-",
+            hint="Exclude",
+            id=f"{k}-exclude",
+            fname=k,
+            options=sorted(map(lambda x: {
+                    "selected": x[0],
+                    "value": f"!{x[1]}",
+                    "display": x[1]
+                }, chain(
+                map(lambda x: (True, x), filter_data[k][1]),
+                map(lambda x: (False, x), set([
+                    x[k] for _, x in group.iterrows()
+                ]) - filter_data[k][1])
+            )), key=lambda x: x["display"] if "display" in x else x["value"]), # exclude
         ) for k in keys ],
         data=[ template(
             'app/rest/data-item',
             product=row['product'],
             category=row['category'],
             group=row['group'],
-            size=36,
         ) for _, row in group.iterrows() ],
-        tag=get_query_param(*filter_data['tag']),
-        tags=[
-            template('app/rest/tag-item', tag=x) for x in tags
-        ],
-        size=36,
+        tags=sorted(map(lambda x: {
+                "selected": x[0],
+                "value": x[1],
+            },chain(
+                map(lambda x: (True, x), filter_data['tag'][0]),
+                map(lambda x: (False, x), set(chain(*data[in_chart]['tags'])) - filter_data['tag'][0])
+        )), key=lambda x: x["display"] if "display" in x else x["value"]), # include,
+        units=sorted(map(lambda x: {
+                "selected": x[0],
+                "value": x[1],
+            },chain(
+                map(lambda x: (True, x), filter_data['unit'][0]),
+                map(lambda x: (False, x), ALL_UNITS - filter_data['unit'][0])
+        )), key=lambda x: x["display"] if "display" in x else x["value"]), # include
     )
 
 

+ 0 - 4
app/rest/tag-item.tpl

@@ -1,4 +0,0 @@
-<tr>
-    <td>{{ tag }}</td>
-    <td></td>
-</tr>

+ 1 - 1
app/rest/trend.tpl

@@ -3,7 +3,7 @@
     <head>
         <style>
 svg {width: 100vw}
-input {max-width: 22vw}
+select {max-width: 23vw}
         </style>
         <title>Trend</title>
         <meta name="viewport" content="width=device-width, initial-scale=1"/>