X-Git-Url: http://rtime.felk.cvut.cz/gitweb/coffee/coffee-flask.git/blobdiff_plain/f9d1adb7a4d9957f523692a5ae392b9f6c5b5b66..HEAD:/app.py diff --git a/app.py b/app.py index 94f438d..473da29 100644 --- a/app.py +++ b/app.py @@ -1,59 +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() ) - return render_template('user.html') + return redirect(url_for('home')) @app.route('/user/rename') @@ -65,21 +156,65 @@ 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) + b = BytesIO() if "uid" in session: uid = session["uid"] - flavors, counts = zip(*db.coffee_flavors(uid)) + flavors, counts = zip(*db.coffee_flavors(uid, days, start)) else: - flavors, counts = zip(*db.coffee_flavors()) + flavors, counts = zip(*db.coffee_flavors(None, days, start)) 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)) - ax.legend(flavors,bbox_to_anchor=(1.0, 1.0)) - ax.set_title("Your taste") - fig.savefig(b, format="svg", bbox_inches="tight") + 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: + ax.set_title("Your taste") + else: + ax.set_title("This week taste") + + 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") @@ -93,21 +228,21 @@ def coffee_graph_history(): else: hist = db.coffee_history() if hist == []: - days = tuple() + unix_days = tuple() counts = tuple() flavors = tuple() else: - days, counts, flavors = zip(*hist) + unix_days, counts, flavors = zip(*hist) 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 days: + for d in unix_days: ll[d] = 0 - for(d, c, f) in zip(days, counts, flavors): + for(d, c, f) in zip(unix_days, counts, flavors): if f is None: continue what_f = 0 @@ -125,19 +260,25 @@ def coffee_graph_history(): ax.bar(range(len(x)), y, bottom=z) z = [sum(i) for i in zip(y, z)] - days = set(days) + unix_days = set(unix_days) xdays = [i.strftime("%a") for i in [ date.today() - timedelta(j - 1) for j in - range(len(days), 0, -1)]] + range(len(unix_days), 0, -1)]] xdays[-1] = "TDY" xdays[-2] = "YDA" - ax.set_xticks(range(len(days))) + ax.set_xticks(range(len(unix_days))) ax.set_xticklabels(xdays) - ax.set_title("Your week") + + if "uid" in session: + ax.set_title("Your week") + else: + 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") @@ -145,16 +286,19 @@ def coffee_graph_history(): def coffee_add(): if request.method == "POST": json = request.json - if json and "uid" in session: - db.add_coffee(session["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') @@ -168,6 +312,27 @@ def js(): def log(): if request.method == "POST": data = request.data.decode("utf-8") - print(data) + print("Log:", data) 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(".coffee.conf", "r") as f: + conf = loads(f.read()) + except: + 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 doesn't like our request! It's discrimination!" + return err