]> rtime.felk.cvut.cz Git - coffee/coffee-flask.git/blob - app.py
Move Events block before user rename/id block
[coffee/coffee-flask.git] / app.py
1 from flask import Flask, render_template, send_file, request, session, redirect, url_for, make_response
2 from flask_cors import CORS
3
4 import numpy as np
5 import matplotlib
6 matplotlib.use('Agg')
7 import matplotlib.pyplot as plt
8 from matplotlib.ticker import MaxNLocator
9 from io import BytesIO
10
11 import coffee_db as db
12 import time
13 import sys
14 from datetime import date, timedelta, datetime, timezone
15
16 from json import loads
17 from requests import post
18 import jinja2
19
20 db.init_db()
21 app = Flask(__name__)
22 CORS(app, supports_credentials=True)
23 app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
24
25
26 # Inspired by https://shubhamjain.co/til/how-to-render-human-readable-time-in-jinja/,
27 # updated to our needs
28 def humanize_ts(time):
29     """
30     Convert date in ISO format to relative, human readable string
31     like 'an hour ago', 'Yesterday', '3 months ago',
32     'just now', etc
33     """
34     if jinja2.is_undefined(time):
35         return time
36     now = datetime.now(timezone.utc)
37     if time[-1] == 'Z':     # Convert Zulu time zone to datetime compatible format
38         time = time[0:-1] + '+00:00'
39     diff = now - datetime.fromisoformat(time)
40     second_diff = diff.seconds
41     day_diff = diff.days
42
43     if day_diff < 0:
44         return ''
45
46     if day_diff == 0:
47         if second_diff < 10:
48             return "just now"
49         if second_diff < 60:
50             return str(int(second_diff)) + " seconds ago"
51         if second_diff < 120:
52             return "a minute ago"
53         if second_diff < 3600:
54             return str(int(second_diff / 60)) + " minutes ago"
55         if second_diff < 7200:
56             return "an hour ago"
57         if second_diff < 86400:
58             return str(int(second_diff / 3600)) + " hours ago"
59     if day_diff == 1:
60         return "Yesterday"
61     if day_diff < 7:
62         return str(day_diff) + " days ago"
63     if day_diff < 31:
64         return str(int(day_diff / 7)) + " weeks ago"
65     if day_diff < 365:
66         return str(int(day_diff / 30)) + " months ago"
67     return str(int(day_diff / 365)) + " years ago"
68
69
70 app.jinja_env.filters['humanize'] = humanize_ts
71
72
73 @app.route('/')
74 def hello():
75     if "uid" in session:
76         uid = session["uid"]
77         return render_template('hello.html', name=db.get_name(uid))
78     return render_template('hello.html')
79
80
81 @app.route('/login', methods=["POST"])
82 @app.route('/login/<iid>')
83 def login(iid=None):
84     if request.method == "POST":
85         iid = request.data.decode("utf-8")
86     if iid is not None:
87         uid = db.get_uid(iid)
88
89         session["iid"] = iid
90
91         if uid is None:
92             db.add_user_identifier(iid, iid, "Default")
93             session["uid"] = iid
94             db.add_user(iid)
95         else:
96             session["uid"] = uid
97     return redirect(url_for('user'))
98
99
100 @app.route('/logout')
101 def logout():
102     session.pop('uid', None)
103     session.pop('iid', None)
104     return redirect(url_for('user'))
105
106
107 @app.route('/user')
108 def user():
109     if "uid" in session:
110         uid = session["uid"]
111         counts = db.drink_count(uid, 0)
112         return render_template('user.html',
113                                name=db.get_name(uid),
114                                flavors=[_name for (_name, _ord) in db.flavors()],
115                                counts=counts,
116                                identifiers=db.list_user_identifiers(uid),
117                                iid=session["iid"],
118                                stamp=time.time(),
119                                last_events=db.last_events()
120                                )
121     # TODO: Replace stamp parameter with proper cache control HTTP
122     # headers in response
123     return render_template('user.html', stamp=time.time(),
124                            last_events=db.last_events(),
125                            )
126
127
128 @app.route('/user/rename')
129 def user_rename():
130     name = request.args.get("name")
131     if name and "uid" in session:
132         uid = session["uid"]
133         db.name_user(uid, name)
134     return redirect(url_for('user'))
135
136
137 @app.route('/user/identifier/add', methods=["POST"])
138 def user_add_identifier():
139     if request.method == "POST":
140         json = request.json
141         if "uid" in session and "id" in json:
142             db.add_user_identifier(session["uid"], json["id"], 'None')
143     return redirect(url_for('user'))
144
145
146 @app.route('/user/identifier/rename', methods=["POST"])
147 def user_rename_identifier():
148     if request.method == "POST":
149         json = request.json
150         if "uid" in session and all(key in json for key in ["id", "name"]):
151             db.rename_user_identifier(session["uid"], json["id"], json["name"])
152     return redirect(url_for('user'))
153
154
155 @app.route('/user/identifier/disable', methods=["POST"])
156 def user_disable_identifier():
157     if request.method == "POST":
158         json = request.json
159         if "uid" in session and "id" in json:
160             db.disable_user_identifier(session["uid"], json["id"])
161     return logout() # force logout
162
163
164 @app.route("/coffee/graph_flavors")
165 def coffee_graph_flavors():
166     days = request.args.get('days', default = 0, type = int)
167     start = request.args.get('start', default = 0, type = int)
168
169     b = BytesIO()
170     if "uid" in session:
171         uid = session["uid"]
172         flavors, counts = zip(*db.coffee_flavors(uid, days, start))
173     else:
174         flavors, counts = zip(*db.coffee_flavors(None, days, start))
175     fig = plt.figure(figsize=(3, 3))
176     ax = fig.add_subplot(111)
177     ax.set_aspect(1)
178     if "normalize" in matplotlib.pyplot.pie.__code__.co_varnames:
179         # Matplotlib >= 3.3.0
180         ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '',
181                normalize=True)
182     else:
183         # Matplotlib < 3.3.0
184         ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '')
185
186     ax.legend(flavors, bbox_to_anchor=(1.0, 1.0))
187
188     if "uid" in session:
189         ax.set_title("Your taste")
190     else:
191         ax.set_title("This week taste")
192
193     fig.savefig(b, format="svg", bbox_inches="tight")
194     b.seek(0)
195     return send_file(b, mimetype="image/svg+xml")
196
197
198 @app.route("/coffee/graph_history")
199 def coffee_graph_history():
200     b = BytesIO()
201     if "uid" in session:
202         uid = session["uid"]
203         hist = db.coffee_history(uid)
204     else:
205         hist = db.coffee_history()
206     if hist == []:
207         unix_days = tuple()
208         counts = tuple()
209         flavors = tuple()
210     else:
211         unix_days, counts, flavors = zip(*hist)
212     fig = plt.figure(figsize=(4, 3))
213     ax = fig.add_subplot(111)
214
215     list_flavor = [_name for (_name, _ord) in sorted(db.flavors(), key=lambda x: x[1])]
216     l = [{} for i in range(len(list_flavor))]
217     for ll in l:
218         for d in unix_days:
219             ll[d] = 0
220
221     for(d, c, f) in zip(unix_days, counts, flavors):
222         if f is None:
223             continue
224         what_f = 0
225         for i in range(len(list_flavor)):
226             if f == list_flavor[i]:
227                 what_f = i
228                 break
229         l[what_f][d] += c
230
231     z = list(0 for i in range(len(l[0])))
232     for flavor in range(len(list_flavor)):
233         sortedlist = [(k, l[flavor][k]) for k in sorted(l[flavor])]
234         x = [i[0] for i in sortedlist]
235         y = [i[1] for i in sortedlist]
236         ax.bar(range(len(x)), y, bottom=z)
237         z = [sum(i) for i in zip(y, z)]
238
239     unix_days = set(unix_days)
240     xdays = [i.strftime("%a") for i in [
241         date.today() - timedelta(j - 1) for j in
242         range(len(unix_days), 0, -1)]]
243     xdays[-1] = "TDY"
244     xdays[-2] = "YDA"
245     ax.set_xticks(range(len(unix_days)))
246     ax.set_xticklabels(xdays)
247
248     if "uid" in session:
249         ax.set_title("Your week")
250     else:
251         ax.set_title("This week total")
252
253     ax.yaxis.set_major_locator(MaxNLocator(integer=True))
254     fig.savefig(b, format="svg", bbox_inches="tight")
255     b.seek(0)
256     plt.close(fig)
257     return send_file(b, mimetype="image/svg+xml")
258
259
260 @app.route("/coffee/add", methods=["POST"])
261 def coffee_add():
262     if request.method == "POST":
263         json = request.json
264
265         if "iid" in session and all(key in json for key in ["flavor", "time"]):
266             print("User/id '%s' had '%s' at %s" % (session["iid"], json["flavor"], json["time"]))
267             db.add_coffee(session["iid"], json["flavor"], json["time"])
268     return redirect(url_for('user'))
269
270
271 @app.route("/event", methods=["POST"])
272 def event_add():
273     json = request.json
274     print("User '%(uid)s' registered event %(event_name)s at %(time)s" % json)
275     db.add_event(json["uid"], json["event_name"], json["time"])
276     return redirect(url_for('user'))
277
278
279 # TODO: Remove me - unused
280 @app.route("/coffee/count")
281 def coffee_count():
282     start = request.args.get("start")
283     stop = request.args.get("stop")
284     return str(dict(db.drink_count(session.get("uid"), start, stop)).get("coffee", 0))
285
286
287 @app.route('/js')
288 def js():
289     response = make_response(render_template('main.js'))
290     response.headers['Content-Type'] = "text/javascript"
291     return response
292
293
294 @app.route("/log", methods=["POST"])
295 def log():
296     if request.method == "POST":
297         data = request.data.decode("utf-8")
298         print("Log:", data)
299         return data
300     return "nope"
301
302 @app.route("/tellCoffeebot", methods=["POST"])
303 def tell_coffeebot():
304     err = "Don't worry now! There is a NEW HOPE Tonda is buying NEW PACK!"
305     if request.method == "POST":
306         what = loads(request.data.decode("utf-8"))
307     try:
308         with open(".coffee.conf", "r") as f:
309             conf = loads(f.read())
310     except:
311         return "Config read error: '%s'! Please find in git history how the .coffee.conf file should look." \
312             % sys.exc_info()[1]
313     try:
314         res = post(conf["coffeebot"]["url"], json=what)
315         print("res is {}".format(res))
316     except:
317         err = "No connection! No covfefe! We all die here!"
318     if not res.ok:
319         err = "Slack doesn't like our request! It's discrimination!"
320     return err