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