1 from http.server import BaseHTTPRequestHandler, HTTPServer
9 def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows):
11 <meta charset="UTF-8">
13 table { margin-bottom: 2em; }
14 th, td { text-align: left; }
15 td.number { text-align: right; }
16 input[type="number"] { text-align: right; }
19 <form action="/" method="POST">
20 <td><input name="update" type="submit" value="update" /></td>
22 <tr><th>eatable</th><th>unit count</th><th>unit weight (g)</th><th>calories</th><th>sugar (g)</th></tr>
28 <input type="hidden" name="keep_visible" value="0">
29 <td><select name="eatable_key">{eatables_selection}</select></td>
30 <td class="number"><input class="unit_count" name="unit_count" type="number" step="0.1" min="0" value="0" /></td>
35 <tr><th>day</th><th>calories</th><th>sugar (g)</th></tr>
39 <tr><th>title</th><th>calories</th><th>sugar (g)</th><th>standard weight (g)</th><th>comments</th><th>delete</th></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>
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;
65 input.insertAdjacentElement('afterend', button);
72 class LockFileDetected(Exception):
77 def __init__(self, title, cals, sugar_g, standard_g=100, comments="", popularity=0):
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
89 "sugar_g": self.sugar_g,
90 "standard_g": self.standard_g,
91 "comments": self.comments,
92 "popularity": self.popularity
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
104 "eatable_key": self.eatable_key,
105 "unit_count": self.unit_count,
106 "keep_visible": self.keep_visible
111 def __init__(self, calories, sugar_g):
112 self.calories = calories
113 self.sugar_g = sugar_g
117 "calories": self.calories,
118 "sugar_g": self.sugar_g,
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"
128 self.consumptions = []
130 self.today = Day(0, 0)
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))
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"]))
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()
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 }
163 def eatables_selection(self):
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:
170 html += '<option value="%s">%s</option>' % (k, v.title)
173 def add_eatable(self, id_, eatable):
174 self.eatables[id_] = eatable
176 def add_consumption(self, consumption):
177 self.consumptions += [consumption]
179 def add_day(self, date, day, archives_today=False):
181 date = date + str(datetime.datetime.now())[10:]
182 self.days[date] = day
184 def set_today_date(self, today_date):
185 self.today_date = today_date
187 def delete(self, id_):
188 del self.eatables[id_]
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+")
198 with open(self.db_file, "w") as f:
199 json.dump(self.to_dict(), f)
200 os.remove(self.lock_file)
203 class MyServer(BaseHTTPRequestHandler):
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)
211 def decode(key, i, is_num=True):
213 return float(postvars[key][i])
214 return postvars[key][i]
216 if 'delete' in postvars.keys():
217 for target in postvars['delete']:
218 to_delete += [target]
220 if 'eatable_uuid' in postvars.keys():
221 for uuid_encoded in postvars['eatable_uuid']:
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)
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)
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))
235 if c.unit_count == 0 and c.keep_visible == 0:
237 db.add_consumption(c)
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)))
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:
252 db.eatables[c.eatable_key].popularity += 1
255 for k, v in sorted(db.eatables.items(), key=lambda item: -item[1].popularity):
256 db.add_consumption(Consumption(k, 0))
258 if (default_slots <= 0):
262 self.send_response(302)
263 self.send_header('Location', '/')
265 except LockFileDetected:
266 self.send_response(400)
268 self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
271 self.send_response(200)
272 self.send_header("Content-type", "text/html")
277 for k,v in db.eatables.items():
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)
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\">"\
294 "<td class\"number\"><input class=\"unit_count number\" name=\"unit_count\" type=\"number\" min=\"0\" step=\"0.1\" value=\"%.1f\" /></td>"\
296 "<td class=\"number\">%.1f</td>"\
297 "<td class=\"number\">%.1f</td>"\
298 "</tr>" % (c.eatable_key, db.eatables[c.eatable_key].title, c.unit_count, r["cals"], r["sugar"])
300 for date in sorted(db.days.keys()):
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
308 "<th>today:</th><th></th><th></th><th>archive?</th>"\
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"))
320 if __name__ == "__main__":
321 webServer = HTTPServer((hostName, serverPort), MyServer)
322 print(f"Server started http://{hostName}:{serverPort}")
324 webServer.serve_forever()
325 except KeyboardInterrupt:
327 webServer.server_close()
328 print("Server stopped.")