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