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