]> rtime.felk.cvut.cz Git - coffee/coffee-flask.git/blob - app.py
2a4b96b988fcee1e9bfb583ef3d07b540f00eb59
[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 @app.route('/')
77 def hello():
78     if "uid" in session:
79         uid = session["uid"]
80         return render_template('hello.html', name=db.get_name(uid))
81     return render_template('hello.html')
82
83
84 @app.route('/login', methods=["POST"])
85 @app.route('/login/<iid>')
86 def login(iid=None):
87     if request.method == "POST":
88         iid = request.data.decode("utf-8")
89     if iid is not None:
90         uid = db.get_uid(iid)
91
92         session["iid"] = iid
93
94         if uid is None:
95             db.add_user_identifier(iid, iid, "Default")
96             session["uid"] = iid
97             db.add_user(iid)
98         else:
99             session["uid"] = uid
100     return redirect(url_for('user'))
101
102
103 @app.route('/logout')
104 def logout():
105     session.pop('uid', None)
106     session.pop('iid', None)
107     return redirect(url_for('user'))
108
109
110 @app.route('/user')
111 def user():
112     if "uid" in session:
113         uid = session["uid"]
114         counts = db.drink_count(uid, 0)
115         return render_template('user.html',
116                                name=db.get_name(uid),
117                                flavors=[_name for (_name, _ord) in db.flavors()],
118                                counts=counts,
119                                identifiers=db.list_user_identifiers(uid),
120                                iid=session["iid"],
121                                stamp=time.time(),
122                                last_events=db.last_events()
123                                )
124     # TODO: Replace stamp parameter with proper cache control HTTP
125     # headers in response
126     return render_template('user.html', stamp=time.time(),
127                            last_events=db.last_events(),
128                            )
129
130
131 @app.route('/user/rename')
132 def user_rename():
133     name = request.args.get("name")
134     if name and "uid" in session:
135         uid = session["uid"]
136         db.name_user(uid, name)
137     return redirect(url_for('user'))
138
139
140 @app.route('/user/identifier/add', methods=["POST"])
141 def user_add_identifier():
142     if request.method == "POST":
143         json = request.json
144         if "uid" in session and "id" in json:
145             db.add_user_identifier(session["uid"], json["id"], 'None')
146     return redirect(url_for('user'))
147
148
149 @app.route('/user/identifier/rename', methods=["POST"])
150 def user_rename_identifier():
151     if request.method == "POST":
152         json = request.json
153         if "uid" in session and all(key in json for key in ["id", "name"]):
154             db.rename_user_identifier(session["uid"], json["id"], json["name"])
155     return redirect(url_for('user'))
156
157
158 @app.route('/user/identifier/disable', methods=["POST"])
159 def user_disable_identifier():
160     if request.method == "POST":
161         json = request.json
162         if "uid" in session and "id" in json:
163             db.disable_user_identifier(session["uid"], json["id"])
164     return logout()  # force logout
165
166
167 @app.route("/coffee/graph_flavors")
168 def coffee_graph_flavors():
169     days = request.args.get('days', default=0, type=int)
170     start = request.args.get('start', default=0, type=int)
171
172     b = BytesIO()
173     if "uid" in session:
174         uid = session["uid"]
175         flavors, counts = zip(*db.coffee_flavors(uid, days, start))
176     else:
177         flavors, counts = zip(*db.coffee_flavors(None, days, start))
178     fig = plt.figure(figsize=(3, 3))
179     ax = fig.add_subplot(111)
180     ax.set_aspect(1)
181     if "normalize" in matplotlib.pyplot.pie.__code__.co_varnames:
182         # Matplotlib >= 3.3.0
183         ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '',
184                normalize=True)
185     else:
186         # Matplotlib < 3.3.0
187         ax.pie(counts, autopct=lambda p: '{:.0f}'.format(p * sum(counts)/100) if p != 0 else '')
188
189     ax.legend(flavors, bbox_to_anchor=(1.0, 1.0))
190
191     if "uid" in session:
192         ax.set_title("Your taste")
193     else:
194         ax.set_title("This week taste")
195
196     fig.savefig(b, format="svg", bbox_inches="tight")
197     b.seek(0)
198     return send_file(b, mimetype="image/svg+xml")
199
200
201 @app.route("/coffee/graph_history")
202 def coffee_graph_history():
203     b = BytesIO()
204     if "uid" in session:
205         uid = session["uid"]
206         hist = db.coffee_history(uid)
207     else:
208         hist = db.coffee_history()
209     if hist == []:
210         unix_days = tuple()
211         counts = tuple()
212         flavors = tuple()
213     else:
214         unix_days, counts, flavors = zip(*hist)
215     fig = plt.figure(figsize=(4, 3))
216     ax = fig.add_subplot(111)
217
218     list_flavor = [_name for (_name, _ord) in sorted(db.flavors(), key=lambda x: x[1])]
219     l = [{} for i in range(len(list_flavor))]
220     for ll in l:
221         for d in unix_days:
222             ll[d] = 0
223
224     for(d, c, f) in zip(unix_days, counts, flavors):
225         if f is None:
226             continue
227         what_f = 0
228         for i in range(len(list_flavor)):
229             if f == list_flavor[i]:
230                 what_f = i
231                 break
232         l[what_f][d] += c
233
234     z = list(0 for i in range(len(l[0])))
235     for flavor in range(len(list_flavor)):
236         sortedlist = [(k, l[flavor][k]) for k in sorted(l[flavor])]
237         x = [i[0] for i in sortedlist]
238         y = [i[1] for i in sortedlist]
239         ax.bar(range(len(x)), y, bottom=z)
240         z = [sum(i) for i in zip(y, z)]
241
242     unix_days = set(unix_days)
243     xdays = [i.strftime("%a") for i in [
244         date.today() - timedelta(j - 1) for j in
245         range(len(unix_days), 0, -1)]]
246     xdays[-1] = "TDY"
247     xdays[-2] = "YDA"
248     ax.set_xticks(range(len(unix_days)))
249     ax.set_xticklabels(xdays)
250
251     if "uid" in session:
252         ax.set_title("Your week")
253     else:
254         ax.set_title("This week total")
255
256     ax.yaxis.set_major_locator(MaxNLocator(integer=True))
257     fig.savefig(b, format="svg", bbox_inches="tight")
258     b.seek(0)
259     plt.close(fig)
260     return send_file(b, mimetype="image/svg+xml")
261
262
263 @app.route("/coffee/add", methods=["POST"])
264 def coffee_add():
265     if request.method == "POST":
266         json = request.json
267
268         if "iid" in session and all(key in json for key in ["flavor", "time"]):
269             print("User/id '%s' had '%s' at %s" % (session["iid"], json["flavor"], json["time"]))
270             db.add_coffee(session["iid"], json["flavor"], json["time"])
271     return redirect(url_for('user'))
272
273
274 @app.route("/event", methods=["POST"])
275 def event_add():
276     json = request.json
277     print("User '%(uid)s' registered event %(event_name)s at %(time)s" % json)
278     db.add_event(json["uid"], json["event_name"], json["time"])
279     return redirect(url_for('user'))
280
281
282 # TODO: Remove me - unused
283 @app.route("/coffee/count")
284 def coffee_count():
285     start = request.args.get("start")
286     stop = request.args.get("stop")
287     return str(dict(db.drink_count(session.get("uid"), start, stop)).get("coffee", 0))
288
289
290 @app.route('/js')
291 def js():
292     response = make_response(render_template('main.js'))
293     response.headers['Content-Type'] = "text/javascript"
294     return response
295
296
297 @app.route("/log", methods=["POST"])
298 def log():
299     if request.method == "POST":
300         data = request.data.decode("utf-8")
301         print("Log:", data)
302         return data
303     return "nope"
304
305
306 @app.route("/tellCoffeebot", methods=["POST"])
307 def tell_coffeebot():
308     err = "Don't worry now! There is a NEW HOPE Tonda is buying NEW PACK!"
309     if request.method == "POST":
310         what = loads(request.data.decode("utf-8"))
311     try:
312         with open(".coffee.conf", "r") as f:
313             conf = loads(f.read())
314     except:
315         return "Config read error: '%s'! Please find in git history how the .coffee.conf file should look." \
316             % sys.exc_info()[1]
317     try:
318         res = post(conf["coffeebot"]["url"], json=what)
319         print("res is {}".format(res))
320     except:
321         err = "No connection! No covfefe! We all die here!"
322     if not res.ok:
323         err = "Slack doesn't like our request! It's discrimination!"
324     return err