From fa2d8b9dd7a26fb2390ff7e69465cbe61d823d94 Mon Sep 17 00:00:00 2001 From: David Huss <dh@atoav.com> Date: Wed, 13 Jan 2021 12:54:54 +0100 Subject: [PATCH] More GET options, changed config, better README --- README.md | 25 ++++++-- stechuhr_server/server.py | 131 ++++++++++++++++++++++++++++++++++---- templates/all.html | 13 ++++ 3 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 templates/all.html diff --git a/README.md b/README.md index 3b20268..1d2e50e 100755 --- a/README.md +++ b/README.md @@ -30,16 +30,27 @@ pip3 install -r requirements.txt python3 stechuhr_server/server.py ``` -Check the output to find the config directory. You might want to run it as a different user tho. -There is also a systemctl unit file that you probably need to change to your needs -In production it makes sense to run stechuhr with gunicorn: +## Deployment -```bash -source env/bin/activate -gunicorn stechuhr_server.server:app -``` +The _stechuhr-server_ is meant to run behind a reverse proxy server (e.g. NGINX) and as a systemd service on a Linux system. Gunicorn acts as a runner. + +Follow the steps listed above in _Run with python3-ven in production_. To install the stechuhr server. If the software runs in an initial test we need to set up a production environment + +1. Create a user called `wwwrun` +2. Copy the systemd unit file `stechuhr-server.service` to `/etc/systemd/system/stechuhr-server.service` and have a look at it. Note the line where it says: + ```Environment="STECHUHR-SERVER_CONFIG_PATH=/etc/stechuhr-server/config.toml"``` + This is where the stechuhr-server default config will be created on first startup. Make sure the user `wwwrun` is allowed to write the file there` +3. Copy the stechuhr-server directory to `/srv/stechuhr-server` +4. Create the directory `/srv/stechuhr-data` and `chown wwwrun:wwwrun /srv/stechuhr-data` +5. Enable the service via `systemctl enable stechuhr-server` +6. Start the service via `systemctl start stechuhr-server` +7. Check the status via `systemctl status stechuhr-server` or display the log via `journalctl -fu stechuhr-server` + +After first start the default config file should be created, have a look at it and change the defaults (e.g. with `vim /etc/stechuhr-server/config.toml`). To update the changes run `systemctl restart stechuhr-server`. + +To make the server reachable from the outside a reverse proxy (like NGINX) needs to be setup. For this have a look at the example configuration file: `stechuhr.server.nginx.config` You may need to change a few things like the host or proxy_pass port. # Configuration diff --git a/stechuhr_server/server.py b/stechuhr_server/server.py index dcfbd0d..045ee57 100644 --- a/stechuhr_server/server.py +++ b/stechuhr_server/server.py @@ -3,7 +3,7 @@ import re import datetime as dt import sqlite3 -from flask import Flask, request +from flask import Flask, request, render_template from logging.config import dictConfig from .config import initialize_config @@ -15,7 +15,10 @@ APPLICATION_NAME = "stechuhr-server" DEFAULT_CONFIG = """ [application] -ignore_get_requests = false +# Warning: setting this to true can will display all visitor movments when the +# server adress is visited. Only use for debugging! +expose_visitor_data = true +expose_current_visitor_number = true [database] # sqlite db path, use ":memory:" for RAM db @@ -145,7 +148,7 @@ def get_records(conn, cursor): # Execute query and fill visitor with results cursor.execute(sqlite_select_query) records = cursor.fetchall() - visitors = [{"location":r[0], "entrance":r[1], "direction":r[2], "time":r[3], "id":r[4]} for r in records] + visitors = [{"location":r[0], "entrance":r[1], "direction":r[2], "time":dt.datetime.strptime(r[3], "%Y-%m-%d %H:%M:%S.%f"), "id":r[4]} for r in records] except sqlite3.OperationalError as e: # If there is no table or no records, just return an empty list app.logger.info('Couldn\'t retrieve visitor records: {}'.format(e)) @@ -250,22 +253,122 @@ def post(): def get(): """ This function runs when a GET request on / is received. - Can be deactivated in the config with the ignore_get_requests setting + Can be deactivated in the config with the expose_visitor_data setting """ - if config["application"]["ignore_get_requests"]: + if not config["application"]["expose_visitor_data"]: app.logger.info('501, Get Request Ignored due to config settings') return "", 501 else: conn = sqlite3.connect(config["database"]["path"]) cursor = conn.cursor() visitors = get_records(conn, cursor) - table = [] - table.append("<table>") - table.append("<tr><th>location</th><th>entrance</th><th>direction</th><th>time</th><th>id</th></tr>") - for v in visitors: - table.append("<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>".format(v["location"], v["entrance"], v["direction"], v["time"], v["id"])) - table.append("</table>") - table = "\n".join(table) conn.close() - app.logger.info('200, Get visitors: {}'.format(table)) - return table, 200 \ No newline at end of file + app.logger.info('200, Returning {} entries from visitor list'.format(len(visitors))) + return render_template('all.html', title=APPLICATION_NAME, visitors=visitors) + + +# This gets run for each request +@app.route('/today', methods = ['GET']) +def today(): + """ + This function runs when a GET request on /today is received. + Can be deactivated in the config with the expose_visitor_data setting + """ + if not config["application"]["expose_visitor_data"]: + app.logger.info('501, Get Request Ignored due to config settings') + return "", 501 + else: + conn = sqlite3.connect(config["database"]["path"]) + cursor = conn.cursor() + visitors = get_records(conn, cursor) + visitors = [v for v in visitors if v["time"].date() == dt.datetime.today().date()] + conn.close() + app.logger.info('200, Returning {} entries from visitor list (all locations)'.format(len(visitors))) + return render_template('all.html', title=APPLICATION_NAME, visitors=visitors) + + +# This gets run for each request +@app.route('/current', methods = ['GET']) +def current(): + """ + This function runs when a GET request on /current is received. + Can be deactivated in the config with the expose_visitor_data setting + """ + if not config["application"]["expose_visitor_data"]: + app.logger.info('501, Get Request Ignored due to config settings') + return "", 501 + else: + conn = sqlite3.connect(config["database"]["path"]) + cursor = conn.cursor() + visitors = get_records(conn, cursor) + visitors = filter_current_visitors(visitors) + + conn.close() + app.logger.info('200, Returning {} entries from visitor list (currently inside all locations)'.format(len(visitors))) + return render_template('all.html', title=APPLICATION_NAME, visitors=visitors) + + +# This gets run for each request +@app.route('/number', methods = ['GET']) +def number(): + """ + This function runs when a GET request on /number is received. + Can be deactivated in the config with the expose_current_visitor_number setting + """ + if not config["application"]["expose_current_visitor_number"]: + app.logger.info('501, Get Request Ignored due to config settings') + return "", 501 + else: + conn = sqlite3.connect(config["database"]["path"]) + cursor = conn.cursor() + visitors = get_records(conn, cursor) + visitors = filter_current_visitors(visitors) + n = len(visitors) + + conn.close() + app.logger.info('200, Returning number of visitors ({}, currently inside all locations)'.format(n)) + return str(n), 200 + + +def filter_current_visitors(visitors: list, hours: int=24) -> list: + """ + Return a list of visitors which are currently inside all locations. + Note: to avoid accumulating errors look only at the last n hours + """ + # Limit to last n hours + visitors = [v for v in visitors if v["time"] > dt.datetime.now() - dt.timedelta(hours=hours)] + + # Get a unique list of IDs present + unique_ids = list(set([v["id"] for v in visitors])) + inside = [] + + # Per ID figure out if the user is inside + for iD in unique_ids: + movements = [v for v in visitors if v["id"] == iD] + incomings = [m for m in movements if m["direction"] == "in"] + outgoings = [m for m in movements if m["direction"] == "out"] + + last_movement = None + + if len(incomings) == 0: + # Visitor was only moving out in the last 24h, so they are not here + continue + elif not len(outgoings) == 0: + # Visitor was moving in AND out in the last 24h + last_in = incomings[-1] + last_out = outgoings[-1] + # Are they still inside the location? + if last_out["time"] < last_in["time"]: + last_movement = last_in + else: + continue + else: + # Visitor was only moving in in the last 24h + last_in = incomings[-1] + last_movement = last_in + + # When the user is still here, append the last movement to the list + if last_movement is not None: + inside.append(last_movement) + + return inside \ No newline at end of file diff --git a/templates/all.html b/templates/all.html new file mode 100644 index 0000000..e6eef62 --- /dev/null +++ b/templates/all.html @@ -0,0 +1,13 @@ +<html> + <head> + <title>{{ title }}</title> + </head> + <body> + <table> + <tr><th></th><th>Location</th><th>Entrance</th><th>ID</th><th>Time</th></tr> + {% for visitor in visitors %} + <tr><td class="direction">{{ visitor.direction }}</td><td class="location">{{ visitor.location }}</td><td class="entrance">{{ visitor.entrance }}</td><td class="id">{{ visitor.id }}</td><td class="time">{{ visitor.time }}</td></tr> + {% endfor %} + </table> + </body> +</html> \ No newline at end of file -- GitLab