home · contact · privacy
Minor layout improvement to calories counter.
[misc] / calories.py
1 from http.server import BaseHTTPRequestHandler, HTTPServer
2 import os
3 import json
4 import datetime
5
6 hostName = "localhost"
7 serverPort = 8081
8
9 def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows):
10     return """<html>
11 <meta charset="UTF-8">
12 <style>
13 table { margin-bottom: 2em; }
14 th, td { text-align: left; }
15 td.number { text-align: right; }
16 input[type="number"] { text-align: right; }
17 </style>""" + f"""
18 <body>
19 <form action="/" method="POST">
20 <td><input name="update" type="submit" value="update" /></td>
21 <table>
22 <tr><th>eatable</th><th>unit count</th><th>unit weight (g)</th><th>calories</th><th>sugar (g)</th></tr>
23 {consumption_rows}
24 <tr>
25 <th>add from DB:</th>
26 </tr>
27 <tr>
28 <input type="hidden" name="keep_visible" value="0">
29 <td class="number"><input class="unit_count" name="unit_count" type="number" step="0.1" min="0" value="0" /></td>
30 <td><select name="eatable_key">{eatables_selection}</select></td>
31 <td></td>
32 </tr>
33 </table>
34 <table>
35 <tr><th>day</th><th>calories</th><th>sugar (g)</th></tr>
36 {day_rows}
37 </table>
38 <table>
39 <tr><th>title</th><th>calories</th><th>sugar (g)</th><th>standard weight (g)</th><th>comments</th><th>delete</th></tr>
40 {eatable_rows}
41 <tr>
42 <th>add:</th>
43 </tr>
44 <tr>
45 <td><input name="title" type="text" value="" /></td>
46 <td class="number"><input name="cals" type="number" min="0" step="0.1" value="0" /></td>
47 <td class="number"><input name="sugar_g" type="number" min="0" step="0.1" value="0" /></td>
48 <td class="number"><input name="standard_g" type="number" min="1" step="0.1" value="1" /></td>
49 <td><input name="comments" type="text" value="" /></td>
50 </tr>
51 </table>
52 </form>
53 </body>
54 <script>
55 """ + """
56 var unit_count_inputs = document.getElementsByClassName("unit_count");
57 for (let i = 0; i < unit_count_inputs.length; i++) {
58     let input = unit_count_inputs[i];
59     let button = document.createElement('button');
60     button.innerHTML = '+1';
61     button.onclick = function(event) {
62         event.preventDefault();
63         input.value = parseFloat(input.value) + 1.0;
64     };
65     input.insertAdjacentElement('afterend', button);
66 }
67
68 </script>
69 </html>
70 """
71
72 class LockFileDetected(Exception):
73     pass
74
75 class Eatable:
76
77     def __init__(self, title, cals, sugar_g, standard_g=100, comments="", popularity=0):
78         self.title = title
79         self.cals = cals  # per 100g
80         self.sugar_g = sugar_g  # per 100g
81         self.standard_g = standard_g  # common unit weight
82         self.comments = comments
83         self.popularity = popularity
84
85     def to_dict(self):
86         return {
87             "title": self.title,
88             "cals": self.cals,
89             "sugar_g": self.sugar_g,
90             "standard_g": self.standard_g,
91             "comments": self.comments,
92             "popularity": self.popularity
93         }
94
95 class Consumption:
96
97     def __init__(self, eatable_key, unit_count=None, keep_visible=0):
98         self.eatable_key = eatable_key
99         self.unit_count = unit_count
100         self.keep_visible = keep_visible
101
102     def to_dict(self):
103         return {
104             "eatable_key": self.eatable_key,
105             "unit_count": self.unit_count,
106             "keep_visible": self.keep_visible
107         }
108
109 class Day:
110
111     def __init__(self, calories, sugar_g):
112         self.calories = calories
113         self.sugar_g = sugar_g
114
115     def to_dict(self):
116         return {
117             "calories": self.calories,
118             "sugar_g": self.sugar_g,
119         }
120
121 class Database:
122
123     def __init__(self, load_from_file=True):
124         db_name = "calories_db"
125         self.db_file = db_name + ".json"
126         self.lock_file = db_name+ ".lock"
127         self.eatables = {}
128         self.consumptions = []
129         self.days = {}
130         self.today = Day(0, 0)
131         self.today_date = ""
132         if load_from_file and os.path.exists(self.db_file):
133             with open(self.db_file, "r") as f:
134                 self.from_dict(json.load(f))
135
136     def from_dict(self, d):
137         self.set_today_date(d["today_date"])
138         for k,v in d["eatables"].items():
139             self.add_eatable(k, Eatable(v["title"], v["cals"], v["sugar_g"], v["standard_g"], v["comments"]))
140         for c in d["consumptions"]:
141             self.add_consumption(Consumption(c["eatable_key"], c["unit_count"]))
142         for k,v in d["days"].items():
143             self.add_day(k, Day(v["calories"], v["sugar_g"]))
144
145     def to_dict(self):
146         d = {"eatables": {}, "consumptions": [], "days":{}, "today_date":self.today_date}
147         for k,v in self.eatables.items():
148             d["eatables"][k] = v.to_dict()
149         for c in self.consumptions:
150             d["consumptions"] += [c.to_dict()]
151         for k,v in self.days.items():
152             d["days"][k] = v.to_dict()
153         return d
154
155     def calc_consumption(self, c):
156         eatable = self.eatables[c.eatable_key]
157         calories = eatable.cals * c.unit_count
158         sugar_g = eatable.sugar_g * c.unit_count
159         self.today.calories += calories
160         self.today.sugar_g += sugar_g
161         return {"cals": calories, "sugar": sugar_g }
162
163     def eatables_selection(self):
164         html = ''
165         already_selected = [c.eatable_key for c in self.consumptions]
166         for k, v in sorted(self.eatables.items(), key=lambda item: item[1].title):
167             if k in already_selected:
168                 continue
169             v = self.eatables[k]
170             html += '<option value="%s">%s</option>' % (k, v.title)
171         return html
172
173     def add_eatable(self, id_, eatable):
174         self.eatables[id_] = eatable
175
176     def add_consumption(self, consumption):
177         self.consumptions += [consumption]
178
179     def add_day(self, date, day, archives_today=False):
180         if archives_today:
181             date = date + str(datetime.datetime.now())[10:]
182         self.days[date] = day
183
184     def set_today_date(self, today_date):
185         self.today_date = today_date
186
187     def delete(self, id_):
188         del self.eatables[id_]
189
190     def write(self):
191         import shutil
192         if os.path.exists(self.lock_file):
193             raise LockFileDetected
194         if os.path.exists(self.db_file):
195             shutil.copy(self.db_file, self.db_file + ".bak")
196         f = open(self.lock_file, "w+")
197         f.close()
198         with open(self.db_file, "w") as f:
199             json.dump(self.to_dict(), f)
200         os.remove(self.lock_file)
201
202
203 class MyServer(BaseHTTPRequestHandler):
204
205     def do_POST(self):
206         from uuid import uuid4
207         from urllib.parse import parse_qs
208         length = int(self.headers['content-length'])
209         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
210         db = Database(False)
211         def decode(key, i, is_num=True):
212             if is_num:
213                 return float(postvars[key][i])
214             return postvars[key][i]
215         to_delete = []
216         if 'delete' in postvars.keys():
217             for target in postvars['delete']:
218                 to_delete += [target]
219         i = 0
220         if 'eatable_uuid' in postvars.keys():
221             for uuid_encoded in postvars['eatable_uuid']:
222                 uuid = uuid_encoded
223                 if uuid not in to_delete:
224                     e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False))
225                     db.add_eatable(uuid, e)
226                 i += 1
227         if 'title' in postvars.keys() and len(postvars['title'][i]) > 0:
228              e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False))
229              db.add_eatable(str(uuid4()), e)
230         i = 0
231         if 'eatable_key' in postvars.keys():
232             for eatable_key in postvars['eatable_key']:
233                 c = Consumption(decode("eatable_key", i, False), decode("unit_count", i), decode("keep_visible", i))
234                 i += 1
235                 if c.unit_count == 0 and c.keep_visible == 0:
236                     continue
237                 db.add_consumption(c)
238         i = 0
239         if 'day_date' in postvars.keys():
240             for date in postvars['day_date']:
241                 db.add_day((date), Day(decode("day_cals", i), decode("day_sugar", i)))
242                 i += 1
243         if 'new_date' in postvars.keys():
244             db.set_today_date(postvars["new_date"][0])
245         if 'archive_day' in postvars.keys():
246             new_cals = postvars["new_day_cals"][0]
247             new_sugar = postvars["new_day_sugar"][0]
248             db.add_day(db.today_date, Day(float(new_cals), float(new_sugar)), archives_today=True)
249             db.set_today_date(str(datetime.datetime.now())[:10])
250             for c in db.consumptions:
251                 if c.unit_count > 0:
252                     db.eatables[c.eatable_key].popularity += 1
253             db.consumptions = []
254             default_slots = 10
255             for k, v in sorted(db.eatables.items(), key=lambda item: -item[1].popularity):
256                 db.add_consumption(Consumption(k, 0))
257                 default_slots -= 1
258                 if (default_slots <= 0):
259                     break
260         try:
261             db.write()
262             self.send_response(302)
263             self.send_header('Location', '/')
264             self.end_headers()
265         except LockFileDetected:
266             self.send_response(400)
267             self.end_headers()
268             self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
269
270     def do_GET(self):
271         self.send_response(200)
272         self.send_header("Content-type", "text/html")
273         self.end_headers()
274         db = Database()
275
276         eatables = ""
277         for k,v in db.eatables.items():
278             eatables += "<tr>"\
279                     "<input name=\"eatable_uuid\" type=\"hidden\" value=\"%s\" />"\
280                     "<td><input name=\"title\" value=\"%s\" /></td>"\
281                     "<td class\"number\"><input name=\"cals\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
282                     "<td class\"number\"><input name=\"sugar_g\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
283                     "<td class\"number\"><input name=\"standard_g\" type=\"number\" step=\"0.1\" min=\"1\" value=\"%1.f\" /></td>"\
284                     "<td><input name=\"comments\" value=\"%s\" /></td>"\
285                     "<td><input name=\"delete\" type=\"checkbox\" value=\"%s\" /></td>"\
286                     "</tr>" % (k, v.title, v.cals, v.sugar_g, v.standard_g, v.comments, k)
287         consumptions = ""
288         db.consumptions = sorted(db.consumptions, key=lambda x: db.eatables[x.eatable_key].title)
289         for c in db.consumptions:
290             r = db.calc_consumption(c)
291             consumptions += "<tr />"\
292                     "<input type=\"hidden\" name=\"keep_visible\" value=\"1\"><input name=\"eatable_key\" type=\"hidden\" value=\"%s\">"\
293                     "<td class\"number\"><input class=\"unit_count number\" name=\"unit_count\" type=\"number\" min=\"0\" step=\"0.1\" value=\"%.1f\" /></td>"\
294                     "<td>%s</td>"\
295                     "<td></td>"\
296                     "<td class=\"number\">%.1f</td>"\
297                     "<td class=\"number\">%.1f</td>"\
298                     "</tr>" % (c.eatable_key, c.unit_count, db.eatables[c.eatable_key].title, r["cals"], r["sugar"])
299         day_rows = ""
300         for date in sorted(db.days.keys()):
301             day = db.days[date]
302             day_rows = "<tr>"\
303                     "<td><input name=\"day_date\" type=\"hidden\" value=\"%s\" />%s</td>"\
304                     "<td class=\"number\"><input name=\"day_cals\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\
305                     "<td class=\"number\"><input name=\"day_sugar\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\
306                     "</tr>" % (date, date[:10], day.calories, day.calories, day.sugar_g, day.sugar_g) + day_rows
307         day_rows = "<tr>"\
308                 "<th>today:</th><th></th><th></th><th>archive?</th>"\
309                 "</tr>"\
310                 "<tr>"\
311                 "<td><input name=\"new_date\" size=8 value=\"%s\" /></td>"\
312                 "<td class=\"number\"><input name=\"new_day_cals\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\
313                 "<td class=\"number\"><input name=\"new_day_sugar\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\
314                 "<td><input name=\"archive_day\" type=\"checkbox\" /></td>"\
315                 "</tr>" % (db.today_date, db.today.calories, db.today.calories, db.today.sugar_g, db.today.sugar_g) + day_rows
316         page = build_page(eatables, consumptions, db.eatables_selection(), day_rows)
317         self.wfile.write(bytes(page, "utf-8"))
318
319
320 if __name__ == "__main__":
321     webServer = HTTPServer((hostName, serverPort), MyServer)
322     print(f"Server started http://{hostName}:{serverPort}")
323     try:
324         webServer.serve_forever()
325     except KeyboardInterrupt:
326         pass
327     webServer.server_close()
328     print("Server stopped.")