home · contact · privacy
Refactor all accounting scripts.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 19 Nov 2023 05:12:13 +0000 (06:12 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 19 Nov 2023 05:12:13 +0000 (06:12 +0100)
calories.py
income_progress_bars.py
ledger.py
plomlib.py

index 5d2fc98c3aaf99ceaeae0708e6f06094370c8bc3..88165b8da1e0be4a9447a9562f81041335cbee1f 100644 (file)
@@ -1,43 +1,76 @@
-from http.server import BaseHTTPRequestHandler, HTTPServer
 import os
 import json
 import datetime
+import jinja2
+from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer
 
-hostName = "localhost"
-serverPort = 8081
+db_path = '/home/plom/org/calories_db.json'
 
-def build_page(eatable_rows, consumption_rows, eatables_selection, day_rows):
-    return """<html>
-<meta charset="UTF-8">
+server_port = 8081
+
+tmpl = """
 <style>
 table { margin-bottom: 2em; }
 th, td { text-align: left; }
 td.number { text-align: right; }
 input[type="number"] { text-align: right; }
-</style>""" + f"""
+</style>
 <body>
 <form action="/" method="POST">
 <td><input name="update" type="submit" value="update" /></td>
 <table>
 <tr><th>eatable</th><th>unit count</th><th>unit weight (g)</th><th>calories</th><th>sugar (g)</th></tr>
-{consumption_rows}
+{% for c in consumptions %}
+<tr>
+<input type="hidden" name="keep_visible" value="1"><input name="eatable_key" type="hidden" value="{{c.key|e}}">
+<td class="number"><input class="unit_count number" name="unit_count" type="number" min="0" step="0.1" value="{{c.count}}" /></td>
+<td>{{c.title}}</td>
+<td></td>
+<td class="number">{{c.cals}}</td>
+<td class="number">{{c.sugar}}</td>
+</tr>
+{% endfor %}
 <tr>
 <th>add from DB:</th>
 </tr>
 <tr>
 <input type="hidden" name="keep_visible" value="0">
 <td class="number"><input class="unit_count" name="unit_count" type="number" step="0.1" min="0" value="0" /></td>
-<td><select name="eatable_key">{eatables_selection}</select></td>
+<td><select name="eatable_key">{% for sel in eatables_selection %}
+<option value="{{sel.0|e}}">{{sel.1|e}}</option>
+{% endfor %}</select></td>
 <td></td>
 </tr>
 </table>
 <table>
+<tr><th>today:</th><th></th><th></th><th>archive?</th></tr>
+<td><input name="new_date" size=8 value="{{db.today_date}}" /><td>
+<td class="number"><input name="new_day_cals" type="hidden" value="{{db.today.calories}}" readonly />{{db.today.calories}}</td>
+<td class="number"><input name="new_day_sugar" type="hidden" value="{{db.today.sugar_g}}" readonly />{{db.today.sugar_g}}</td>
+<td><input name="archive_day" type="checkbox" /></td>
+</tr>
 <tr><th>day</th><th>calories</th><th>sugar (g)</th></tr>
-{day_rows}
+{% for d in days %}
+<tr>
+<td><input name="day_date" type="hidden" value="{{d.date|e}}" />{{d.date_short|e}}</td>
+<td class="number"><input name="day_cals" type="hidden" step="0.1" min="0" value="{{d.cals}}" />{{d.cals}}</td>
+<td class="number"><input name="day_sugar" type="hidden" step="0.1" min="0" value="{{d.sugar}}" />{{d.sugar}}</td>
+</tr>
+{% endfor %}
 </table>
 <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}
+{% for e in eatables %}
+<tr>
+<input name="eatable_uuid" type="hidden" value="{{e.uuid}}" />
+<td><input name="title" value="{{e.title|e}}" /></td>
+<td class="number"><input name="cals" type="number" step="0.1" min="0" value="{{e.cals}}" /></td>
+<td class="number"><input name="sugar_g" type="number" step="0.1" min="0" value="{{e.sugar_g}}" /></td>
+<td class="number"><input name="standard_g" type="number" step="0.1" min="0" value="{{e.sugar_g}}" /></td>
+<td><input name="comments" value="{{e.comments|e}}" /</td>
+<td><input name="delete" type="checkbox" value="{{e.uuid}}" />
+</tr>
+{% endfor %}
 <tr>
 <th>add:</th>
 </tr>
@@ -52,7 +85,6 @@ input[type="number"] { text-align: right; }
 </form>
 </body>
 <script>
-""" + """
 var unit_count_inputs = document.getElementsByClassName("unit_count");
 for (let i = 0; i < unit_count_inputs.length; i++) {
     let input = unit_count_inputs[i];
@@ -66,11 +98,8 @@ for (let i = 0; i < unit_count_inputs.length; i++) {
 }
 
 </script>
-</html>
 """
 
-class LockFileDetected(Exception):
-    pass
 
 class Eatable:
 
@@ -92,6 +121,7 @@ class Eatable:
             "popularity": self.popularity
         }
 
+
 class Consumption:
 
     def __init__(self, eatable_key, unit_count=None, keep_visible=0):
@@ -106,6 +136,7 @@ class Consumption:
             "keep_visible": self.keep_visible
         }
 
+
 class Day:
 
     def __init__(self, calories, sugar_g):
@@ -118,20 +149,22 @@ class Day:
             "sugar_g": self.sugar_g,
         }
 
-class Database:
+
+class CaloriesDB(PlomDB):
 
     def __init__(self, load_from_file=True):
-        db_name = "calories_db"
-        self.db_file = db_name + ".json"
-        self.lock_file = db_name+ ".lock"
+        self.load_from_file = load_from_file
         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))
+        super().__init__(db_path)
+
+    def read_db_file(self, f):
+        if not self.load_from_file:
+            return
+        self.from_dict(json.load(f))
 
     def from_dict(self, d):
         self.set_today_date(d["today_date"])
@@ -161,14 +194,14 @@ class Database:
         return {"cals": calories, "sugar": sugar_g }
 
     def eatables_selection(self):
-        html = ''
+        options = []
         already_selected = [c.eatable_key for c in self.consumptions]
         for k, v in sorted(self.eatables.items(), key=lambda item: item[1].title):
             if k in already_selected:
                 continue
             v = self.eatables[k]
-            html += '<option value="%s">%s</option>' % (k, v.title)
-        return html
+            options += [(k, v.title)] 
+        return options 
 
     def add_eatable(self, id_, eatable):
         self.eatables[id_] = eatable
@@ -188,26 +221,17 @@ class Database:
         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)
+        self.write_text_to_db(json.dumps(self.to_dict()))
 
 
-class MyServer(BaseHTTPRequestHandler):
+class CaloriesServer(PlomServer):
 
     def do_POST(self):
         from uuid import uuid4
         from urllib.parse import parse_qs
         length = int(self.headers['content-length'])
         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
-        db = Database(False)
+        db = CaloriesDB(False)
         def decode(key, i, is_num=True):
             if is_num:
                 return float(postvars[key][i])
@@ -259,70 +283,51 @@ class MyServer(BaseHTTPRequestHandler):
                     break
         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"))
+            self.redirect()
+        except PlomException as e:
+            self.fail_400(e) 
 
     def do_GET(self):
-        self.send_response(200)
-        self.send_header("Content-type", "text/html")
-        self.end_headers()
-        db = Database()
-
-        eatables = ""
+        db = CaloriesDB()
+        # eatables = ""
+        eatable_rows = []
         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 class\"number\"><input name=\"cals\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
-                    "<td class\"number\"><input name=\"sugar_g\" type=\"number\" step=\"0.1\" min=\"0\" value=\"%.1f\" /></td>"\
-                    "<td class\"number\"><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 = ""
+            eatable_rows += [{
+               'uuid': k,
+               'title': v.title,
+               'cals': f'{v.cals:.1f}',
+               'sugar_g': f'{v.sugar_g:.1f}',
+               'standard_g': f'{v.standard_g:.1f}',
+               'comments': v.comments
+            }]
         db.consumptions = sorted(db.consumptions, key=lambda x: db.eatables[x.eatable_key].title)
+        consumption_rows = []
         for c in db.consumptions:
             r = db.calc_consumption(c)
-            consumptions += "<tr />"\
-                    "<input type=\"hidden\" name=\"keep_visible\" value=\"1\"><input name=\"eatable_key\" type=\"hidden\" value=\"%s\">"\
-                    "<td class\"number\"><input class=\"unit_count number\" name=\"unit_count\" type=\"number\" min=\"0\" step=\"0.1\" value=\"%.1f\" /></td>"\
-                    "<td>%s</td>"\
-                    "<td></td>"\
-                    "<td class=\"number\">%.1f</td>"\
-                    "<td class=\"number\">%.1f</td>"\
-                    "</tr>" % (c.eatable_key, c.unit_count, db.eatables[c.eatable_key].title, r["cals"], r["sugar"])
-        day_rows = ""
-        for date in sorted(db.days.keys()):
+            consumption_rows += [{
+                'key': c.eatable_key,
+                'count': c.unit_count,
+                'title': db.eatables[c.eatable_key].title,
+                'cals': r['cals'],
+                'sugar': r['sugar']
+            }]
+        day_rows = []
+        for date in reversed(sorted(db.days.keys())):
             day = db.days[date]
-            day_rows = "<tr>"\
-                    "<td><input name=\"day_date\" type=\"hidden\" value=\"%s\" />%s</td>"\
-                    "<td class=\"number\"><input name=\"day_cals\" type=\"hidden\" step=\"0.1\" min=\"0\" value=\"%.1f\" />%.1f</td>"\
-                    "<td class=\"number\"><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
-        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 class=\"number\"><input name=\"new_day_cals\" type=\"hidden\" value=\"%.1f\" readonly />%.1f</td>"\
-                "<td class=\"number\"><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) + day_rows
-        page = build_page(eatables, consumptions, db.eatables_selection(), day_rows)
-        self.wfile.write(bytes(page, "utf-8"))
-
-
-if __name__ == "__main__":
-    webServer = HTTPServer((hostName, serverPort), MyServer)
-    print(f"Server started http://{hostName}:{serverPort}")
-    try:
-        webServer.serve_forever()
-    except KeyboardInterrupt:
-        pass
-    webServer.server_close()
-    print("Server stopped.")
+            day_rows += [{
+                'date': date,
+                'date_short': date[:10],
+                'cals': f'{day.calories:.1f}',
+                'sugar': f'{day.sugar_g:.1f}',
+            }]
+        page = jinja2.Template(tmpl).render(
+                db=db,
+                days=day_rows,
+                consumptions=consumption_rows,
+                eatables=eatable_rows,
+                eatables_selection=db.eatables_selection())
+        self.send_HTML(page)
+
+
+if __name__ == "__main__":  
+    run_server(server_port, CaloriesServer)
index 94a34569c70909a290c82ad21eb6bba8cf8a6196..553627d3788f2f44a0be3fa605453e021f79ac27 100644 (file)
@@ -1,13 +1,12 @@
-# from http.server import BaseHTTPRequestHandler, HTTPServer
 import os
 import json
 import jinja2
 from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer
 
-server_port = 8081
+server_port = 8083
+db_path = '/home/plom/org/income.json'
 
-tmpl = jinja2.Template("""<html>
-<meta charset="UTF-8">
+tmpl = """
 <style>
 body {
   font-family: monospace;
@@ -78,20 +77,20 @@ table {
 </style>
 <body>
 <table>
-<tr><th /><th>earned</th><th>progress</th><th>surplus</th></tr >
-{% for p in progress_bars %}
-<tr><th>{{p.title}}</th>
+<tr><th></th><th>earned</th><th>progress</th><th>surplus</th></tr >
+{% for p in progress_bars %}<tr><th>{{p.title}}</th>
 <td class="countable">{{p.earned|round(2)}}</td>
-<td class="progressbar">{% if p.time_progress >= 0 %}<div class="time_progress" style="margin-left: {{p.time_progress}}px"></div>{% endif %}<div class="progress" style="background-color: {% if p.success < 0.5 %}red{% elif p.success < 1 %}yellow{% else %}green{% endif %}; width: {{p.success_income_cut}}"></div></td>
-<td class="progressbar surplusbar"><div class="diff_goal">{{p.diff_goal}}</div><div class="progressbar surplus" style="width: {{p.success_income_bonus}}" /></div></td></tr>
-{% endfor %}
-</table>
+<td class="progressbar">{% if p.time_progress >= 0 %}<div class="time_progress" style="margin-left: {{p.time_progress}}px"></div>{% endif %}<div class="progress" style="background-color: {% if p.success < 0.5 %}red{% elif p.success < 1 %}yellow{% else %}green{% endif %}; width: {{p.success_income_cut}}px"></div></td>
+<td class="progressbar surplusbar"><div class="diff_goal">{{p.diff_goal}}</div><div class="progressbar surplus" style="width: {{p.success_income_bonus}}px" ></div></td></tr>
+{% endfor %}</table>
+
 <form action="/" method="POST">
 <table>
 <tr><th>hourly rate</th><th>worked today</th></tr>
 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_1" value="{{workday_hourly_rate_1}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_1" value="{{workday_minutes_worked_1}}" step="5" /> minutes</td>
 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_2" value="{{workday_hourly_rate_2}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_2" value="{{workday_minutes_worked_2}}" step="5" /> minutes</td>
 <tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_3" value="{{workday_hourly_rate_3}}"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_3" value="{{workday_minutes_worked_3}}" step="5" /> minutes</td>
+</table>
 <table>
 <tr><th>yearly income goal</th><td><input type="number" class="year_goal" min="1" name="year_goal" value="{{year_goal}}" />€</td></tr>
 <tr><th>monthly income goal</th><td class="countable">{{month_goal|round(2)}}€</td></tr>
@@ -103,8 +102,8 @@ table {
 <input type="submit" name="update" value="update inputs" />
 <input type="submit" name="finish" value="finish day" />
 </form>
-</body
-</html>""")
+"""
+
 
 class IncomeDB(PlomDB):
 
@@ -124,7 +123,7 @@ class IncomeDB(PlomDB):
         self.workday_minutes_worked_3 = 0,
         self.year_goal = 20000,
         self.workdays_per_month = 16
-        super().__init__('_income')
+        super().__init__(db_path)
 
     def read_db_file(self, f):
         d = json.load(f)
@@ -143,6 +142,7 @@ class IncomeDB(PlomDB):
     def write_db(self):
         self.write_text_to_db(json.dumps(self.to_dict()))
 
+
 class ProgressBar:
     def __init__(self, title, earned, goal, time_progress=-1):
         self.title = title
@@ -160,7 +160,7 @@ class ProgressBar:
             if time_progress > 0:
                 self.success = success_income / time_progress
 
-# class MyServer(BaseHTTPRequestHandler):
+
 class IncomeServer(PlomServer):
 
     def do_POST(self):
@@ -188,9 +188,7 @@ class IncomeServer(PlomServer):
                 db.workday_minutes_worked_2 = 0
                 db.workday_minutes_worked_3 = 0
             db.write_db()
-            self.send_response(302)
-            self.send_header('Location', '/')
-            self.end_headers()
+            self.redirect()
         except PlomException as e:
             self.fail_400(e) 
 
@@ -237,7 +235,7 @@ class IncomeServer(PlomServer):
                              ProgressBar("month", month_plus, month_goal, progress_time_month),
                              ProgressBar("week", week_plus, week_goal, progress_time_week),
                              ProgressBar("workday", day_income, workday_goal)]
-            page = tmpl.render(
+            page = jinja2.Template(tmpl).render(
                     progress_bars = progress_bars,
                     workday_hourly_rate_1 = db.workday_hourly_rate_1,
                     workday_minutes_worked_1 = db.workday_minutes_worked_1,
@@ -256,5 +254,6 @@ class IncomeServer(PlomServer):
         except PlomException as e:
             self.fail_400(e) 
 
+
 if __name__ == "__main__":       
     run_server(server_port, IncomeServer)
index b4d9fd99191af8cdf3a45d2686654b4b371eb3fe..244a68444c6d60901330cd3df6ff637be24553f6 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -1,6 +1,4 @@
-from http.server import BaseHTTPRequestHandler, HTTPServer
 import os
-import html
 import jinja2
 import decimal
 from datetime import datetime, timedelta
@@ -8,6 +6,89 @@ from urllib.parse import parse_qs, urlparse
 from plomlib import PlomDB, PlomException, run_server, run_server, PlomServer
 
 server_port = 8082
+db_path = '/home/plom/org/ledger2023.dat'
+
+html_head = """
+<style>
+body { color: #000000; }
+table { margin-bottom: 2em; }
+th, td { text-align: left }
+input[type=number] { text-align: right; font-family: monospace; }
+.money { font-family: monospace; text-align: right; }
+.comment { font-style: italic; color: #777777; }
+.meta { font-size: 0.75em; color: #777777; }
+.full_line_comment { display: block; white-space: nowrap; width: 0; }
+</style>
+<body>
+<a href="/">ledger</a>
+<a href="/balance">balance</a>
+<a href="/add_free">add free</a>
+<a href="/add_structured">add structured</a>
+<hr />
+"""
+booking_html = """
+<p id="{{nth}}"><a href="#{{nth}}">{{date}}</a> {{desc}} <span class="comment">{{head_comment|e}}</span><br />
+<span class="meta">[edit: <a href="/add_structured?start={{start}}&end={{end}}">structured</a>
+/ <a href="/add_free?start={{start}}&end={{end}}">free</a>
+| copy:<a href="/copy_structured?start={{start}}&end={{end}}">structured</a>
+/ <a href="/copy_free?start={{start}}&end={{end}}">free</a>
+| move {% if move_up %}<a href="/move_up?start={{start}}&end={{end}}">up</a>{% else %}up{% endif %}/{% if move_down %}<a href="/move_down?start={{start}}&end={{end}}">down</a>{% else %}down{% endif %}
+| <a href="/balance?stop={{nth+1}}">balance after</a>
+]</span>
+<table>
+{% for l in booking_lines %}
+{% if l.acc %}
+<tr><td>{{l.acc|e}}</td><td class="money">{{l.money|e}}</td><td class="comment">{{l.comment|e}}</td></tr>
+{% else %}
+<tr><td><div class="comment full_line_comment">{{l.comment|e}}</div></td></tr>
+{% endif %}
+{% endfor %}
+</table></p>
+"""
+add_form_header = """<form method="POST" action="{{action|e}}">
+<input type="submit" name="check" value="check" />
+<input type="submit" name="revert" value="revert" />
+"""
+add_form_footer = """
+<input type="hidden" name="start" value={{start}} />
+<input type="hidden" name="end" value={{end}} />
+<input type="submit" name="save" value="save!">
+</form>
+"""
+add_free_html = """<br />
+<textarea name="booking" rows=10 cols=80>
+{% for line in lines %}{{ line }}
+{% endfor %}
+</textarea>
+"""
+add_structured_html = """
+<input type="submit" name="add_taxes" value="add taxes" />
+<input type="submit" name="add_taxes2" value="add taxes2" />
+<input type="submit" name="add_sink" value="add sink" />
+<br />
+<input name="date" value="{{date|e}}" size=9 />
+<input name="description" value="{{desc|e}}" list="descriptions" />
+<textarea name="line_0_comment" rows=1 cols=20>{{head_comment|e}}</textarea>
+<input type="submit" name="line_0_add" value="[+]" />
+<br />
+{% for line in booking_lines %}
+<input name="line_{{line.i}}_account" value="{{line.acc|e}}" size=40 list="accounts" />
+<input type="number" name="line_{{line.i}}_amount" step=0.01 value="{{line.amt}}" size=10 />
+<input name="line_{{line.i}}_currency" value="{{line.curr|e}}" size=3 list="currencies" />
+<input type="submit" name="line_{{line.i}}_delete" value="[x]" />
+<input type="submit" name="line_{{line.i}}_delete_after" value="[XX]" />
+<input type="submit" name="line_{{line.i}}_add" value="[+]" />
+<textarea name="line_{{line.i}}_comment" rows=1 cols={% if line.comm_cols %}{{line.comm_cols}}{% else %}20{% endif %}>{{line.comment|e}}</textarea>
+<br />
+{% endfor %}
+{% for name, items in datalist_sets.items() %}
+<datalist id="{{name}}">
+{% for item in items %}
+  <option value="{{item|e}}">{{item|e}}</option>
+{% endfor %}
+</datalist>
+{% endfor %}
+"""
 
 
 def apply_booking_to_account_balances(account_sums, account, currency, amount):
@@ -256,7 +337,7 @@ class LedgerDB(PlomDB):
         self.bookings = []
         self.comments = []
         self.real_lines = []
-        super().__init__('_ledger')
+        super().__init__(db_path)
         ret = parse_lines(self.real_lines)
         self.bookings += ret[0]
         self.comments += ret[1]
@@ -433,55 +514,9 @@ class LedgerDB(PlomDB):
 
 
 class LedgerServer(PlomServer):
-    header = """<html>
-<meta charset="UTF-8">
-<style>
-body { color: #000000; }
-table { margin-bottom: 2em; }
-th, td { text-align: left }
-input[type=number] { text-align: right; font-family: monospace; }
-.money { font-family: monospace; text-align: right; }
-.comment { font-style: italic; color: #777777; }
-.meta { font-size: 0.75em; color: #777777; }
-.full_line_comment { display: block; white-space: nowrap; width: 0; }
-</style>
-<body>
-<a href="/">ledger</a>
-<a href="/balance">balance</a>
-<a href="/add_free">add free</a>
-<a href="/add_structured">add structured</a>
-<hr />
-"""
-    booking_tmpl = jinja2.Template("""
-<p id="{{nth}}"><a href="#{{nth}}">{{date}}</a> {{desc}} <span class="comment">{{head_comment|e}}</span><br />
-<span class="meta">[edit: <a href="/add_structured?start={{start}}&end={{end}}">structured</a>
-/ <a href="/add_free?start={{start}}&end={{end}}">free</a>
-| copy:<a href="/copy_structured?start={{start}}&end={{end}}">structured</a>
-/ <a href="/copy_free?start={{start}}&end={{end}}">free</a>
-| move {% if move_up %}<a href="/move_up?start={{start}}&end={{end}}">up</a>{% else %}up{% endif %}/{% if move_down %}<a href="/move_down?start={{start}}&end={{end}}">down</a>{% else %}down{% endif %}
-| <a href="/balance?stop={{nth+1}}">balance after</a>
-]</span>
-<table>
-{% for l in booking_lines %}
-{% if l.acc %}
-<tr><td>{{l.acc|e}}</td><td class="money">{{l.money|e}}</td><td class="comment">{{l.comment|e}}</td></tr>
-{% else %}
-<tr><td><div class="comment full_line_comment">{{l.comment|e}}</div></td></tr>
-{% endif %}
-{% endfor %}
-</table></p>
-""")
-    add_form_header = """<form method="POST" action="{{action|e}}">
-<input type="submit" name="check" value="check" />
-<input type="submit" name="revert" value="revert" />
-"""
-    add_form_footer = """
-<input type="hidden" name="start" value={{start}} />
-<input type="hidden" name="end" value={{end}} />
-<input type="submit" name="save" value="save!">
-</form>
-"""
-    footer = "</body>\n<html>"
+
+    def pre_init(self):
+        self.html_head += [html_head]
 
     def do_POST(self):
         try:
@@ -522,15 +557,14 @@ input[type=number] { text-align: right; font-family: monospace; }
                     nth = db.get_nth_for_booking_of_start_line(new_start)
                     if new_start > start: 
                         nth -= 1 
-                redir_url = f'/#{nth}'
-                self.send_code_and_headers(302, [('Location', redir_url)])
+                self.redirect( f'/#{nth}')
             # otherwise just re-build editing form
             else:
                 if '/add_structured' == parsed_url.path: 
                     edit_content = self.add_structured(db, start, end, temp_lines=lines, add_empty_line=add_empty_line)
                 else:
                     edit_content = self.add_free(db, start, end)
-                self.send_HTML(self.header + edit_content + self.footer)
+                self.send_HTML(edit_content)
         except PlomException as e:
             self.fail_400(e)
 
@@ -541,29 +575,27 @@ input[type=number] { text-align: right; font-family: monospace; }
             start = int(params.get('start', ['0'])[0])
             end = int(params.get('end', ['0'])[0])
             db = LedgerDB()
-            page = self.header
             if parsed_url.path == '/balance':
                 stop = params.get('stop', [None])[0]
-                page += self.balance_as_html(db, stop)
+                page = self.balance_as_html(db, stop)
             elif parsed_url.path == '/add_free':
-                page += self.add_free(db, start, end)
+                page = self.add_free(db, start, end)
             elif parsed_url.path == '/add_structured':
-                page += self.add_structured(db, start, end)
+                page = self.add_structured(db, start, end)
             elif parsed_url.path == '/copy_free':
-                page += self.add_free(db, start, end, copy=True)
+                page = self.add_free(db, start, end, copy=True)
             elif parsed_url.path == '/copy_structured':
-                page += self.add_structured(db, start, end, copy=True)
+                page = self.add_structured(db, start, end, copy=True)
             elif parsed_url.path == '/move_up':
                 nth = self.move_up(db, start, end)
-                self.send_code_and_headers(302, [('Location', f'/#{nth}')])
+                self.redirect(f'/#{nth}')
                 return
             elif parsed_url.path == '/move_down':
                 nth = self.move_down(db, start, end)
-                self.send_code_and_headers(302, [('Location', f'/#{nth}')])
+                self.redirect(f'/#{nth}')
                 return
             else:
-                page += self.ledger_as_html(db)
-            page += self.footer
+                page = self.ledger_as_html(db)
             self.send_HTML(page)
         except PlomException as e:
             self.fail_400(e)
@@ -644,6 +676,7 @@ input[type=number] { text-align: right; font-family: monospace; }
         return f"<pre>{content}</pre>"
 
     def ledger_as_html(self, db):
+        booking_tmpl = jinja2.Template(booking_html)
         single_c_tmpl = jinja2.Template('<span class="comment">{{c|e}}</span><br />')  ##
         elements_to_write = []
         last_i = i = 0  ##
@@ -665,7 +698,7 @@ input[type=number] { text-align: right; font-family: monospace; }
                  if booking_line[1] is not None:
                      money = f'{booking_line[1]} {booking_line[2]}'
                  booking_lines += [{'acc': booking_line[0], 'money':money, 'comment':comment}]  ##
-            elements_to_write += [self.booking_tmpl.render(
+            elements_to_write += [booking_tmpl.render(
                 nth=nth,
                 start=booking.start_line,
                 end=booking_end,
@@ -679,46 +712,14 @@ input[type=number] { text-align: right; font-family: monospace; }
         return '\n'.join(elements_to_write)
 
     def add_free(self, db, start=0, end=0, copy=False):
-        tmpl = jinja2.Template(self.add_form_header + """<br />
-<textarea name="booking" rows=10 cols=80>
-{% for line in lines %}{{ line }}
-{% endfor %}
-</textarea>
-""" + self.add_form_footer)
+        tmpl = jinja2.Template(add_form_header + add_free_html + add_form_footer) 
         lines = db.get_lines(start, end)
         if copy:
             start = end = 0
         return tmpl.render(action='add_free', start=start, end=end, lines=lines)
 
     def add_structured(self, db, start=0, end=0, copy=False, temp_lines=[], add_empty_line=None):
-        tmpl = jinja2.Template(self.add_form_header + """
-<input type="submit" name="add_taxes" value="add taxes" />
-<input type="submit" name="add_taxes2" value="add taxes2" />
-<input type="submit" name="add_sink" value="add sink" />
-<br />
-<input name="date" value="{{date|e}}" size=9 />
-<input name="description" value="{{desc|e}}" list="descriptions" />
-<textarea name="line_0_comment" rows=1 cols=20>{{head_comment|e}}</textarea>
-<input type="submit" name="line_0_add" value="[+]" />
-<br />
-{% for line in booking_lines %}
-<input name="line_{{line.i}}_account" value="{{line.acc|e}}" size=40 list="accounts" />
-<input type="number" name="line_{{line.i}}_amount" step=0.01 value="{{line.amt}}" size=10 />
-<input name="line_{{line.i}}_currency" value="{{line.curr|e}}" size=3 list="currencies" />
-<input type="submit" name="line_{{line.i}}_delete" value="[x]" />
-<input type="submit" name="line_{{line.i}}_delete_after" value="[XX]" />
-<input type="submit" name="line_{{line.i}}_add" value="[+]" />
-<textarea name="line_{{line.i}}_comment" rows=1 cols={% if line.comm_cols %}{{line.comm_cols}}{% else %}20{% endif %}>{{line.comment|e}}</textarea>
-<br />
-{% endfor %}
-{% for name, items in datalist_sets.items() %}
-<datalist id="{{name}}">
-{% for item in items %}
-  <option value="{{item|e}}">{{item|e}}</option>
-{% endfor %}
-</datalist>
-{% endfor %}
-""" + self.add_form_footer)
+        tmpl = jinja2.Template(add_form_header + add_structured_html + add_form_footer) 
         lines = temp_lines if len(''.join(temp_lines)) > 0 else db.get_lines(start, end)
         bookings, comments = parse_lines(lines, validate_bookings=False)
         if len(bookings) > 1:
index 9dac71284a5c990514c0e2c89bbd6f8a16e8a55d..92bfb8535d7604bc1e47f0595cc68fd28e433283 100644 (file)
@@ -9,10 +9,10 @@ class PlomException(Exception):
 class PlomDB:
 
     def __init__(self, db_name):
-        self.db_file = db_name + ".json"
-        self.lock_file = db_name+ ".lock"
+        self.db_file = db_name
+        self.lock_file = db_name+ '.lock'
         if os.path.exists(self.db_file):
-            with open(self.db_file, "r") as f:
+            with open(self.db_file, 'r') as f:
                 self.read_db_file(f)
 
     def lock(self):
@@ -78,27 +78,24 @@ class PlomDB:
 
 
 class PlomServer(BaseHTTPRequestHandler): 
-    header = ''
-    footer = ''
-
-    def run(self, port):
-        from http.server import HTTPServer
-        webServer = HTTPServer(('localhost', port), type(self))
-        print(f"Server started http://localhost:{port}")
-        try:
-            webServer.serve_forever()
-        except KeyboardInterrupt:
-            pass
-        webServer.server_close()
-        print("Server stopped.")
+    
+    def __init__(self, *args, **kwargs):
+        self.html_head = ['<!DOCTYPE html>\n<html>\n<meta charset="UTF-8">']
+        self.html_foot = ['</body>\n</html>']
+        self.pre_init()
+        super().__init__(*args, **kwargs)
+
+    def pre_init(self):
+        pass
 
     def fail_400(self, e):
-        page = f'{self.header}ERROR: {e}{self.footer}'
-        self.send_HTML(page, 400)
+        self.send_HTML(f'ERROR: {e}', 400)
 
     def send_HTML(self, html, code=200):
         self.send_code_and_headers(code, [('Content-type', 'text/html')])
-        self.wfile.write(bytes(html, "utf-8"))
+        header = '\n'.join(self.html_head)
+        footer = '\n'.join(self.html_foot)
+        self.wfile.write(bytes(f'{header}\n{html}\n{footer}', 'utf-8'))
 
     def send_code_and_headers(self, code, headers=[]):
         self.send_response(code)
@@ -106,6 +103,10 @@ class PlomServer(BaseHTTPRequestHandler):
             self.send_header(fieldname, content)
         self.end_headers()
 
+    def redirect(self, url='/'):
+        self.send_code_and_headers(302, [('Location', url)])
+
+
 
 def run_server(port, server_class):
     from http.server import HTTPServer