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