From e6eeaa6d863dfcef8bf60715e418b875a3dcc800 Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Sat, 30 Sep 2023 01:10:23 +0200
Subject: [PATCH] Add food tracker.

---
 calories.py | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 293 insertions(+)
 create mode 100644 calories.py

diff --git a/calories.py b/calories.py
new file mode 100644
index 0000000..771b11a
--- /dev/null
+++ b/calories.py
@@ -0,0 +1,293 @@
+from http.server import BaseHTTPRequestHandler, HTTPServer
+import os
+import json
+
+def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows):
+    return """<html>
+<meta charset="UTF-8">
+<style>
+table {
+  margin-bottom: 2em;
+}
+td, th {
+  text-align: right;
+}
+</style>""" + f"""
+<body>
+<form action="/" method="POST">
+<td><input name="update" type="submit" value="update" /></td>
+<table>
+<tr><th>title</th><th>calories</th><th>sugar (g)</th><th>standard weight (g)</th><th>comments</th><th>delete</th></tr>
+{eatable_rows}
+<tr>
+<th>add:</th>
+</tr>
+<tr>
+<td><input name="title" type="text" value="" /></td>
+<td><input name="cals" type="number" min="0" step="0.1" value="0" /></td>
+<td><input name="sugar_g" type="number" min="0" step="0.1" value="0" /></td>
+<td><input name="standard_g" type="number" min="1" step="0.1" value="1" /></td>
+<td><input name="comments" type="text" value="" /></td>
+</tr>
+</table>
+<table>
+<tr><th>day</th><th>calories</th><th>sugar (g)</th></tr>
+{day_rows}
+</table>
+<table>
+<tr><th>eatable</th><th>unit count</th><th>unit weight (g)</th><th>calories</th><th>sugar (g)</th></tr>
+{consumption_rows}
+<tr>
+<th>add from DB:</th>
+</tr>
+<tr>
+<td><select name="eatable_key">{eatables_selection}</select></td>
+<td><input name="unit_count" type="number" step="1" min="0" value="0" /></td>
+<td></td>
+</tr>
+</table>
+</form>
+</body>
+</html>"""
+
+class LockFileDetected(Exception):
+    pass
+
+class Eatable:
+
+    def __init__(self, title, cals, sugar_g, standard_g=100, comments=""):
+        self.title = title
+        self.cals = cals  # per 100g
+        self.sugar_g = sugar_g  # per 100g
+        self.standard_g = standard_g  # common unit weight
+        self.comments = comments 
+
+    def to_dict(self):
+        return {
+            "title": self.title,
+            "cals": self.cals,
+            "sugar_g": self.sugar_g,
+            "standard_g": self.standard_g,
+            "comments": self.comments
+        }
+
+class Consumption:
+
+    def __init__(self, eatable_key, unit_count=None):
+        self.eatable_key = eatable_key 
+        self.unit_count = unit_count
+
+    def to_dict(self):
+        return {
+            "eatable_key": self.eatable_key,
+            "unit_count": self.unit_count,
+        }
+
+class Day:
+
+    def __init__(self, calories, sugar_g):
+        self.calories = calories
+        self.sugar_g = sugar_g
+
+    def to_dict(self):
+        return {
+            "calories": self.calories,
+            "sugar_g": self.sugar_g,
+        }
+
+class Database: 
+
+    def __init__(self, load_from_file=True):
+        db_name = "calories_db"
+        self.db_file = db_name + ".json"
+        self.lock_file = db_name+ ".lock"
+        self.eatables = {}
+        self.consumptions = []
+        self.days = {}
+        self.today = Day(0, 0)
+        self.today_date = ""
+        if load_from_file and os.path.exists(self.db_file):
+            with open(self.db_file, "r") as f:
+                self.from_dict(json.load(f))
+
+    def from_dict(self, d):
+        self.set_today_date(d["today_date"])
+        for k,v in d["eatables"].items():
+            self.add_eatable(k, Eatable(v["title"], v["cals"], v["sugar_g"], v["standard_g"], v["comments"]))
+        for c in d["consumptions"]:
+            self.add_consumption(Consumption(c["eatable_key"], c["unit_count"]))
+        for k,v in d["days"].items():
+            self.add_day(k, Day(v["calories"], v["sugar_g"]))
+
+    def to_dict(self):
+        d = {"eatables": {}, "consumptions": [], "days":{}, "today_date":self.today_date}
+        for k,v in self.eatables.items():
+            d["eatables"][k] = v.to_dict()
+        for c in self.consumptions:
+            d["consumptions"] += [c.to_dict()]
+        for k,v in self.days.items():
+            d["days"][k] = v.to_dict()
+        return d
+
+    def calc_consumption(self, c):
+        eatable = self.eatables[c.eatable_key]
+        calories = eatable.cals * c.unit_count
+        sugar_g = eatable.sugar_g * c.unit_count
+        # calories = float(eatable.cals / eatable.standard_g) * c.unit_count * c.unit_weight
+        # sugar_g = float(eatable.sugar_g / eatable.standard_g) * c.unit_count * c.unit_weight
+        self.today.calories += calories
+        self.today.sugar_g += sugar_g
+        return {"cals": calories, "sugar": sugar_g }
+
+    def eatables_selection(self, selection=None):
+        html = '' # if selection else '<option value="" />'
+        for k,v in self.eatables.items():
+            selected = ' selected' if k==selection else ''
+            html += '<option value="%s"%s>%s</option>' % (k, selected, v.title)
+        return html
+
+    def add_eatable(self, id_, eatable):
+        self.eatables[id_] = eatable
+
+    def add_consumption(self, consumption):
+        self.consumptions += [consumption] 
+
+    def add_day(self, date, day):
+        self.days[date] = day 
+
+    def set_today_date(self, today_date):
+        self.today_date = today_date
+
+    def delete(self, id_):
+        del self.eatables[id_]
+
+    def write(self):
+        import shutil
+        if os.path.exists(self.lock_file):
+            raise LockFileDetected
+        if os.path.exists(self.db_file):
+            shutil.copy(self.db_file, self.db_file + ".bak")
+        f = open(self.lock_file, "w+")
+        f.close()
+        with open(self.db_file, "w") as f:
+            json.dump(self.to_dict(), f)
+        os.remove(self.lock_file)
+
+
+class MyServer(BaseHTTPRequestHandler):
+
+    def do_POST(self):
+        from uuid import uuid4
+        from urllib.parse import parse_qs
+        import datetime
+        length = int(self.headers['content-length'])
+        postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
+        db = Database(False)
+        def decode(key, i, is_num=True):
+            if is_num:
+                return float(postvars[key][i])
+            return postvars[key][i]
+        to_delete = []
+        if 'delete' in postvars.keys():
+            for target in postvars['delete']:
+                to_delete += [target]
+        i = 0
+        if 'eatable_uuid' in postvars.keys():
+            for uuid_encoded in postvars['eatable_uuid']:
+                uuid = uuid_encoded
+                if uuid not in to_delete:
+                    e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False))
+                    db.add_eatable(uuid, e)
+                i += 1    
+        if 'title' in postvars.keys() and len(postvars['title'][i]) > 0:
+             e = Eatable(decode("title", i, False), decode("cals", i), decode("sugar_g", i), decode("standard_g", i), decode("comments", i, False))
+             db.add_eatable(str(uuid4()), e)
+        i = 0
+        if 'eatable_key' in postvars.keys():
+            for eatable_key in postvars['eatable_key']:
+                c = Consumption(decode("eatable_key", i, False), decode("unit_count", i))
+                i += 1 
+                if c.unit_count == 0:
+                    continue
+                db.add_consumption(c)
+        i = 0
+        if 'day_date' in postvars.keys():
+            for date in postvars['day_date']:
+                db.add_day((date), Day(decode("day_cals", i), decode("day_sugar", i)))
+                i += 1 
+        if 'new_date' in postvars.keys():
+            db.set_today_date(postvars["new_date"][0])
+        if 'archive_day' in postvars.keys():
+            new_cals = postvars["new_day_cals"][0]
+            new_sugar = postvars["new_day_sugar"][0]
+            db.add_day(db.today_date, Day(float(new_cals), float(new_sugar)))
+            db.set_today_date(str(datetime.datetime.now()))#[:10])
+            db.consumptions = []
+        try:
+            db.write()
+            self.send_response(302)
+            self.send_header('Location', '/')
+            self.end_headers()
+        except LockFileDetected:
+            self.send_response(400)
+            self.end_headers()
+            self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
+
+    def do_GET(self):
+        self.send_response(200)
+        self.send_header("Content-type", "text/html")
+        self.end_headers()
+        db = Database()
+
+        eatables = ""
+        for k,v in db.eatables.items():
+            eatables += "<tr>"\
+                    "<input name=\"eatable_uuid\" type=\"hidden\" value=\"%s\" />"\
+                    "<td><input name=\"title\" value=\"%s\" /></td>"\
+                    "<td><input name=\"cals\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
+                    "<td><input name=\"sugar_g\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
+                    "<td><input name=\"standard_g\" type=\"number\" step=\"0.1\" min=\"1\" value=\"%1.f\" /></td>"\
+                    "<td><input name=\"comments\" value=\"%s\" /></td>"\
+                    "<td><input name=\"delete\" type=\"checkbox\" value=\"%s\" /></td>"\
+                    "</tr>" % (k, v.title, v.cals, v.sugar_g, v.standard_g, v.comments, k)
+        consumptions = ""
+        for c in db.consumptions:
+            r = db.calc_consumption(c)
+            consumptions += "<tr />"\
+                    "<td><select name=\"eatable_key\">%s</select></td>"\
+                    "<td><input name=\"unit_count\" type=\"number\" min=\"0\" value=\"%d\" /></td>"\
+                    "<td></td>"\
+                    "<td>%.1f</td>"\
+                    "<td>%.1f</td>"\
+                    "</tr>" % (db.eatables_selection(c.eatable_key), c.unit_count, r["cals"], r["sugar"])
+        day_rows = ""
+        for date, day in db.days.items():
+            day_rows += "<tr>"\
+                    "<td><input name=\"day_date\" type=\"hidden\" value=\"%s\" />%s</td>"\
+                    "<td><input name=\"day_cals\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\
+                    "<td><input name=\"day_sugar\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\
+                    "</tr>" % (date, date[:10], day.calories, day.calories, day.sugar_g, day.sugar_g)
+        day_rows += "<tr>"\
+                "<th>today:</th><th></th><th></th><th>archive?</th>"\
+                "</tr>"\
+                "<tr>"\
+                "<td><input name=\"new_date\" size=8 value=\"%s\" /></td>"\
+                "<td><input name=\"new_day_cals\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\
+                "<td><input name=\"new_day_sugar\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\
+                "<td><input name=\"archive_day\" type=\"checkbox\" /></td>"\
+                "</tr>" % (db.today_date, db.today.calories, db.today.calories, db.today.sugar_g, db.today.sugar_g)
+        page = build_page(eatables, consumptions, db.eatables_selection(), day_rows)
+        self.wfile.write(bytes(page, "utf-8"))
+
+
+hostName = "localhost"
+serverPort = 8080
+if __name__ == "__main__":        
+    webServer = HTTPServer((hostName, serverPort), MyServer)
+    print("Server started http://%s:%s" % (hostName, serverPort))
+    try:
+        webServer.serve_forever()
+    except KeyboardInterrupt:
+        pass
+    webServer.server_close()
+    print("Server stopped.")
-- 
2.30.2