X-Git-Url: http://rtime.felk.cvut.cz/gitweb/coffee/coffee-flask.git/blobdiff_plain/8af719922cabdd070bfd4ba2d9fdfef84c2594d7..HEAD:/app.py diff --git a/app.py b/app.py index 389aa28..473da29 100644 --- a/app.py +++ b/app.py @@ -1,64 +1,150 @@ from flask import Flask, render_template, send_file, request, session, redirect, url_for, make_response from flask_cors import CORS -import numpy as np import matplotlib -matplotlib.use('Agg') import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator from io import BytesIO import coffee_db as db import time -from datetime import date, timedelta +import sys +from datetime import date, timedelta, datetime, timezone from json import loads from requests import post +import jinja2 +import threading + +matplotlib.use('Agg') db.init_db() app = Flask(__name__) CORS(app, supports_credentials=True) app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' +# Workaround for +# https://github.com/matplotlib/matplotlib/issues/13723. Despite the +# issue is closed, the problem is still there. See +# https://github.com/matplotlib/matplotlib/issues/13723#issuecomment-761302131. +plot_lock = threading.Lock() + +# Inspired by https://shubhamjain.co/til/how-to-render-human-readable-time-in-jinja/, +# updated to our needs +def humanize_ts(time, max_interval="years"): + """Convert date/time in ISO format to relative, human readable string. + + Example return values: 'an hour ago', 'Yesterday', '3 months ago', + 'just now', etc. + + When optional max_interval is set to "days", the return value will + report at most the number of days ago, not week, months or years. + """ + if jinja2.is_undefined(time): + return time + if max_interval not in ["years", "days"]: + raise ValueError + + diff = datetime.now(timezone.utc) - time + second_diff = diff.seconds + day_diff = diff.days + + if day_diff < 0: + return 'in the future' + + if day_diff == 0: + if second_diff < 10: + return "just now" + if second_diff < 60: + return str(int(second_diff)) + " seconds ago" + if second_diff < 120: + return "a minute ago" + if second_diff < 3600: + return str(int(second_diff / 60)) + " minutes ago" + if second_diff < 7200: + return "an hour ago" + if second_diff < 86400: + return str(int(second_diff / 3600)) + " hours ago" + if day_diff == 1: + return "Yesterday" + if day_diff < 7 or max_interval == "days": + return str(day_diff) + " days ago" + if day_diff < 31: + return str(int(day_diff / 7)) + " weeks ago" + if day_diff < 365: + return str(int(day_diff / 30)) + " months ago" + return str(int(day_diff / 365)) + " years ago" + + +app.jinja_env.filters['humanize'] = humanize_ts + + +def days_filter(time): + """Return the number of days elapsed since time.""" + if jinja2.is_undefined(time): + return time + diff = datetime.now(timezone.utc) - time + return diff.days + + +app.jinja_env.filters['days'] = days_filter + @app.route('/') def hello(): - if "uid" in session: - uid = session["uid"] - return render_template('hello.html', name=db.get_name(uid)) return render_template('hello.html') +@app.route('/home') +def home(): + # TODO: Replace stamp parameter with proper cache control HTTP + # headers in response + return render_template('home.html', + stamp=time.time(), + last_events=db.last_events()) + + @app.route('/login', methods=["POST"]) -@app.route('/login/') -def login(uid=None): +@app.route('/login/') +def login(iid=None): if request.method == "POST": - uid = request.data.decode("utf-8") - if uid is not None: - db.add_user(uid) - session["uid"] = uid + iid = request.data.decode("utf-8") + if iid is not None: + uid = db.get_uid(iid) + + session["iid"] = iid + + if uid is None: + db.add_user_identifier(iid, iid, "Default") + session["uid"] = iid + db.add_user(iid) + else: + session["uid"] = uid return redirect(url_for('user')) @app.route('/logout') def logout(): session.pop('uid', None) - return redirect(url_for('user')) + session.pop('iid', None) + return redirect(url_for('home')) @app.route('/user') def user(): if "uid" in session: uid = session["uid"] + counts = db.drink_count(uid, 0) return render_template('user.html', name=db.get_name(uid), - flavors=db.flavors(), - count=db.coffee_count(uid, 0), - stamp=time.time() + flavors=[_name for (_name, _ord) in db.flavors()], + counts=counts, + identifiers=db.list_user_identifiers(uid), + iid=session["iid"], + stamp=time.time(), + last_events=db.last_events() ) - # TODO: Replace stamp parameter with proper cache control HTTP - # headers in response - return render_template('user.html', stamp=time.time()) + return redirect(url_for('home')) @app.route('/user/rename') @@ -70,10 +156,37 @@ def user_rename(): return redirect(url_for('user')) +@app.route('/user/identifier/add', methods=["POST"]) +def user_add_identifier(): + if request.method == "POST": + json = request.json + if "uid" in session and "id" in json: + db.add_user_identifier(session["uid"], json["id"], 'None') + return redirect(url_for('user')) + + +@app.route('/user/identifier/rename', methods=["POST"]) +def user_rename_identifier(): + if request.method == "POST": + json = request.json + if "uid" in session and all(key in json for key in ["id", "name"]): + db.rename_user_identifier(session["uid"], json["id"], json["name"]) + return redirect(url_for('user')) + + +@app.route('/user/identifier/disable', methods=["POST"]) +def user_disable_identifier(): + if request.method == "POST": + json = request.json + if "uid" in session and "id" in json: + db.disable_user_identifier(session["uid"], json["id"]) + return logout() # force logout + + @app.route("/coffee/graph_flavors") def coffee_graph_flavors(): - days = request.args.get('days', default = 0, type = int) - start = request.args.get('start', default = 0, type = int) + days = request.args.get('days', default=0, type=int) + start = request.args.get('start', default=0, type=int) b = BytesIO() if "uid" in session: @@ -84,7 +197,14 @@ def coffee_graph_flavors(): fig = plt.figure(figsize=(3, 3)) ax = fig.add_subplot(111) ax.set_aspect(1) - ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '') + if "normalize" in matplotlib.pyplot.pie.__code__.co_varnames: + # Matplotlib >= 3.3.0 + ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '', + normalize=sum(counts) != 0) + else: + # Matplotlib < 3.3.0 + ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '') + ax.legend(flavors, bbox_to_anchor=(1.0, 1.0)) if "uid" in session: @@ -92,7 +212,9 @@ def coffee_graph_flavors(): else: ax.set_title("This week taste") - fig.savefig(b, format="svg", bbox_inches="tight") + with plot_lock: + fig.savefig(b, format="svg", bbox_inches="tight") + plt.close(fig) b.seek(0) return send_file(b, mimetype="image/svg+xml") @@ -114,7 +236,7 @@ def coffee_graph_history(): fig = plt.figure(figsize=(4, 3)) ax = fig.add_subplot(111) - list_flavor = sorted(db.flavors()) + list_flavor = [_name for (_name, _ord) in sorted(db.flavors(), key=lambda x: x[1])] l = [{} for i in range(len(list_flavor))] for ll in l: for d in unix_days: @@ -153,9 +275,10 @@ def coffee_graph_history(): ax.set_title("This week total") ax.yaxis.set_major_locator(MaxNLocator(integer=True)) - fig.savefig(b, format="svg", bbox_inches="tight") + with plot_lock: + fig.savefig(b, format="svg", bbox_inches="tight") + plt.close(fig) b.seek(0) - plt.close(fig) return send_file(b, mimetype="image/svg+xml") @@ -163,16 +286,19 @@ def coffee_graph_history(): def coffee_add(): if request.method == "POST": json = request.json - print("User '%(uid)s' had '%(flavor)s' at %(time)s" % json) - db.add_coffee(json["uid"], json["flavor"], json["time"]) + + if "iid" in session and all(key in json for key in ["flavor", "time"]): + print("User/id '%s' had '%s' at %s" % (session["iid"], json["flavor"], json["time"])) + db.add_coffee(session["iid"], json["flavor"], json["time"]) return redirect(url_for('user')) -@app.route("/coffee/count") -def coffee_count(): - start = request.args.get("start") - stop = request.args.get("stop") - return str(db.coffee_count(session.get("uid"), start, stop)) +@app.route("/event", methods=["POST"]) +def event_add(): + json = request.json + print("User '%(uid)s' registered event %(event_name)s at %(time)s" % json) + db.add_event(json["uid"], json["event_name"], json["time"]) + return redirect(url_for('user')) @app.route('/js') @@ -190,21 +316,23 @@ def log(): return data return "nope" + @app.route("/tellCoffeebot", methods=["POST"]) def tell_coffeebot(): err = "Don't worry now! There is a NEW HOPE Tonda is buying NEW PACK!" if request.method == "POST": what = loads(request.data.decode("utf-8")) try: - with open(".config", "r") as f: + with open(".coffee.conf", "r") as f: conf = loads(f.read()) except: - return "Config needed! Please find in git history how it should look." + return "Config read error: '%s'! Please find in git history how the .coffee.conf file should look." \ + % sys.exc_info()[1] try: res = post(conf["coffeebot"]["url"], json=what) print("res is {}".format(res)) except: err = "No connection! No covfefe! We all die here!" if not res.ok: - err = "Slack don't like the request! It's discrimination!" + err = "Slack doesn't like our request! It's discrimination!" return err