11 Commits
8 changed files with 162 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
FROM debian:latest as builder
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y elm-compiler make ca-certificates
COPY . /app
WORKDIR /app
RUN rm -rf .venv venv
RUN make frontend
FROM python:3.11-alpine as runner
COPY --from=builder /app /app
WORKDIR /app
RUN pip install -r requirements.txt
RUN pip install gunicorn
EXPOSE 5000
ENV JON_DB_CONNECTION_STRING="host=fsmi-db.fsmi.org dbname=garfield"
ENV JON_SECRET_KEY="changeme"
CMD ["sh", "-c", "gunicorn -b '0.0.0.0:5000' --chdir /app 'jon:create_app()'"]
+28
View File
@@ -4,6 +4,11 @@
## Setup ## Setup
`jon` is a Python WSGI application written using Flask.
This means you'll have to install a bunch of Python packages to get up and running.
### Dependencies
``` ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
@@ -11,6 +16,14 @@ pip install -r requirements.txt
You should probably use a virtualenv for that. You should probably use a virtualenv for that.
I develop `jon` using Python 3.10 but it should work with older versions as well. I develop `jon` using Python 3.10 but it should work with older versions as well.
#### Arch Linux
If you're on Arch, these packages are required for running the server:
```
python python-flask python-flask-login python-psycopg2
```
### Building Frontend JS ### Building Frontend JS
Most of jon works without JS but there are some features that require it. Most of jon works without JS but there are some features that require it.
@@ -32,6 +45,20 @@ flask --app jon run --debug
`--debug` restarts the server when a source file changes. `--debug` restarts the server when a source file changes.
## Running with docker
When you prefer running the application using docker you can just use
```
docker compose up
```
In case your local username does not line up with your FSMI-username, you need to specify your FSMI-username
using `USER=<username>`, e.g.:
```
USER=shirkanesi docker compose up
```
This can also be persisted by following the instructions in the docker-compose.yml
## fsmi-db forward ## fsmi-db forward
``` ```
@@ -55,3 +82,4 @@ ssh -nNTvL 5432:fsmi-db.fsmi.org:5432 fsmi-login.fsmi.uni-karlsruhe.de
- [x] Figure out/Add documentation about building `entry.js` - [x] Figure out/Add documentation about building `entry.js`
- [ ] Clean up the code a little and add some comments - [ ] Clean up the code a little and add some comments
- [ ] Needs good documentation for maintainability - [ ] Needs good documentation for maintainability
- [ ] Use cool new function for deactivating items
+19
View File
@@ -0,0 +1,19 @@
---
version: '3.7'
services:
jon:
container_name: jon
build: .
ports:
- "5000:5000"
volumes:
- ~/.pgpass:/root/.pgpass:ro
dns:
- 1.1.1.1
- 8.8.8.8
network_mode: bridge
environment:
# If your local user is different from your fsmi user, change $USER here!
- JON_DB_CONNECTION_STRING="host=fsmi-db.fsmi.org dbname=garfield user=$USER"
- JON_SECRET_KEY="changemetosomethingsuperrandomandsecure"
+1
View File
@@ -20,6 +20,7 @@ def create_app():
# You don't need a config.json. If you don't provide one, default-config.json # You don't need a config.json. If you don't provide one, default-config.json
# is used. # is used.
app.config.from_file("config.json", load=json.load, silent=True) app.config.from_file("config.json", load=json.load, silent=True)
app.config.from_prefixed_env(prefix="JON")
db.init_app(app) db.init_app(app)
auth.init_app(app) auth.init_app(app)
+53
View File
@@ -58,3 +58,56 @@ FROM garfield.inventory_items
) m USING (item_id) ) m USING (item_id)
ORDER BY inventory_items.name; ORDER BY inventory_items.name;
-- How many *other* active inventory lines exist with the same barcode in the same location that are newer?
CREATE TEMPORARY VIEW more_recent_inventory_lines_with_same_barcode AS
SELECT
a.item_id,
-- It's important not to count(*) here because item_id is NULL
-- when no other inventory lines exist.
count(b.item_id) AS other_lines_count
FROM garfield.inventory_items AS a
LEFT JOIN garfield.inventory_items AS b
ON a.item_barcode = b.item_barcode
AND a.item_id != b.item_id
AND a.location = b.location
AND b.available
AND b.bought > a.bought
GROUP BY a.item_id;
-- We need to create this table so that we can put an index on it
-- Otherwise the join in the consumption graph query becomes much slower
-- Perhaps it would be nicer to use a materialized view instead
DROP TABLE IF EXISTS last_n_days;
CREATE TEMPORARY TABLE last_n_days AS (
SELECT
generate_series(now() - interval '14 days', now(), interval '1 day')::date AS sale_date
);
CREATE UNIQUE INDEX last_n_days_sale_date ON last_n_days (sale_date);
-- Get an array of how often items were sold over the last 14 days
CREATE TEMPORARY VIEW inventory_last_n_days_sales AS
WITH
sales_by_date AS (
SELECT
inventory_line AS item_id,
date_trunc('day', snack_sales_log_timestamp AT TIME ZONE 'UTC+1') AS sale_date,
count(*)::int AS sales
FROM garfield.snack_sales_log
GROUP BY item_id, sale_date
),
beeg AS (
SELECT item_id, sale_date, count(snack_sales_log_timestamp)::int AS sales
FROM garfield.inventory_items
CROSS JOIN last_n_days
LEFT JOIN garfield.snack_sales_log
ON inventory_line = item_id
-- snack_sales_log has an index on snack_sales_log_timestamp to speed up this query
-- If that index doesn't exist the query takes much longer.
AND sale_date = date_trunc('day', snack_sales_log_timestamp AT TIME ZONE 'UTC+1')
WHERE available
GROUP BY item_id, sale_date
ORDER BY item_id, sale_date
)
SELECT item_id, array_agg(sales) AS last_n_days_sales
FROM beeg
GROUP BY item_id
+2
View File
@@ -1,6 +1,8 @@
SELECT SELECT
* *
FROM all_inventory_item_overview FROM all_inventory_item_overview
LEFT JOIN more_recent_inventory_lines_with_same_barcode USING (item_id)
LEFT JOIN inventory_last_n_days_sales USING (item_id)
WHERE (%(location_id)s IS NULL OR location = %(location_id)s) WHERE (%(location_id)s IS NULL OR location = %(location_id)s)
AND available AND available
ORDER BY ORDER BY
+4
View File
@@ -74,3 +74,7 @@ th {
details { details {
font-size: 0.8em; font-size: 0.8em;
} }
.consumption-graph {
display: block;
height: 1em;
}
+30
View File
@@ -1,9 +1,34 @@
{% macro consumption_graph_svg(values) -%}
{% set stroke_width = 8 %}
{% set width = 300 %}
{% set height = 100 %}
{% set padding = 4 %}
{% set dx = (width - 2 * padding) / ((values | length) + 1) %}
{% set dy = (height - 2 * padding) / ((values + [1]) | max) %}
<svg viewBox="0 0 {{ width }} {{ height }}" role="img" class="consumption-graph">
<polyline
points="
{% for value in values %}
{{ padding + loop.index * dx }}, {{ height - padding - value * dy }}
{% endfor %}
"
stroke="green"
stroke-width="{{ stroke_width }}"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
>
</svg>
{% endmacro -%}
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<table> <table>
<tr> <tr>
<th>Stat</th>
<th>ID</th> <th>ID</th>
<th>Graph</th>
<th>Barcode</th> <th>Barcode</th>
<th>Name</th> <th>Name</th>
<th>Preis (Netto)</th> <th>Preis (Netto)</th>
@@ -17,7 +42,12 @@
</tr> </tr>
{% for item in items %} {% for item in items %}
<tr> <tr>
<td>
{% if item.units_left == 0 %}<span title="Leerer aktiver Inventareintrag">🅾️</span>{% endif %}
{% if item.other_lines_count != 0 %}<span title="{% if item.other_lines_count == 1 %}Neuerer Eintrag mit demselben Barcode ist aktiv{% else %}{{ item.other_lines_count }} Einträge mit demselben Barcode sind aktiv{% endif %}">🔄</span>{% endif %}
</td>
<td><a href="/inventory/item/{{ item.item_id }}">{{ item.item_id }}</a></td> <td><a href="/inventory/item/{{ item.item_id }}">{{ item.item_id }}</a></td>
<td>{{ consumption_graph_svg(item.last_n_days_sales) }}</td>
<td><code>{{ item.item_barcode }}</code></td> <td><code>{{ item.item_barcode }}</code></td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td class="--align-right">{{ format_currency(item.unit_price) }}</td> <td class="--align-right">{{ format_currency(item.unit_price) }}</td>