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