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