home · contact · privacy
Improve income_progress_bars.py.
[misc] / income_progress_bars.py
index 5a4f5ad013eae7e8bbc68b301e1d429c78e23b72..3edb48dc75068267e00900759587d79542d59076 100644 (file)
@@ -1,11 +1,12 @@
 from http.server import BaseHTTPRequestHandler, HTTPServer
 import os
 import json
+import jinja2
 
 hostName = "localhost"
-serverPort = 8080
+serverPort = 8081
 
-header = """<html>
+tmpl = jinja2.Template("""<html>
 <meta charset="UTF-8">
 <style>
 body {
@@ -33,8 +34,8 @@ td, th {
   height: 20px;
   background-color: white;
   width: 2px;
-  border-left: 1px solid black; 
-  border-right: 1px solid black; 
+  border-left: 1px solid black;
+  border-right: 1px solid black;
   z-index: 2;
 }
 .progress {
@@ -78,186 +79,252 @@ table {
 <body>
 <table>
 <tr><th /><th>earned</th><th>progress</th><th>surplus</th></tr >
-"""
-footer = """</table>
+{% 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>
 <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="%s"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_1" value="%s" step="5" /> minutes</td>
-<tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_2" value="%s"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_2" value="%s" step="5" /> minutes</td>
-<tr><td class="input_container"><input type="number" min="1" class="rate" name="workday_hourly_rate_3" value="%s"/>€</td><td><input type="number" min="0" class="minutes" name="workday_minutes_worked_3" value="%s" step="5" /> minutes</td>
+<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>
-<tr><th>yearly income goal</th><td><input type="number" class="year_goal" min="1" name="year_goal" value="%s" />€</td></tr>
-<tr><th>monthly income goal</th><td class="countable">%.2f€</td></tr>
-<tr><th>weekly income goal</th><td class="countable">%.2f€</td></tr>
-<tr><th>workdays per month</th><td class="input_container"><input type="number" class="workdays" min="1" max="28" name="workdays_per_month" value="%s" /></td></tr>
-<tr><th>workday income goal</th><td class="countable">%.2f€</td></tr>
-<tr><th>workdays per week</th><td class="countable">%.2f€</td></tr>
+<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>
+<tr><th>weekly income goal</th><td class="countable">{{week_goal|round(2)}}€</td></tr>
+<tr><th>workdays per month</th><td class="input_container"><input type="number" class="workdays" min="1" max="28" name="workdays_per_month" value="{{workdays_per_month}}" /></td></tr>
+<tr><th>workday income goal</th><td class="countable">{{workday_goal|round(2)}}€</td></tr>
+<tr><th>workdays per week</th><td class="countable">{{workdays_per_week|round(2)}}</td></tr>
 </table>
 <input type="submit" name="update" value="update inputs" />
 <input type="submit" name="finish" value="finish day" />
 </form>
 </body
-</html>"""
+</html>""")
 
-db_default = {
-  "timestamp_year": 0,
-  "timestamp_month": 0,
-  "timestamp_week": 0,
-  "year_income": 0,
-  "month_income": 0,
-  "week_income": 0,
-  "workday_hourly_rate_1": 10,
-  "workday_minutes_worked_1": 0,
-  "workday_hourly_rate_2": 25,
-  "workday_minutes_worked_2": 0,
-  "workday_hourly_rate_3": 50,
-  "workday_minutes_worked_3": 0,
-  "year_goal": 20000,
-  "workdays_per_month": 16 
-}
-db_file = "db.json"
-lock_file = "db.lock"
-def load_db():
-    if os.path.exists(db_file):
-        with open(db_file, "r") as f:
-            return json.load(f)
-    else:
-        return db_default
+class Database:
+    timestamp_year = 0,
+    timestamp_month = 0,
+    timestamp_week = 0,
+    year_income = 0,
+    month_income = 0,
+    week_income = 0,
+    workday_hourly_rate_1 = 10,
+    workday_hourly_rate_2 = 25,
+    workday_hourly_rate_3 = 50,
+    workday_minutes_worked_1 = 0,
+    workday_minutes_worked_2 = 0,
+    workday_minutes_worked_3 = 0,
+    year_goal = 20000,
+    workdays_per_month = 16
+
+    def __init__(self):
+        db_name = "_income"
+        self.db_file = db_name + ".json"
+        self.lock_file = db_name+ ".lock"
+        if os.path.exists(self.db_file):
+            with open(self.db_file, "r") as f:
+                d = json.load(f)
+                for k, v in d.items():
+                    if not hasattr(self, k):
+                        raise Exception("bad key in db: " + k)
+                    setattr(self, k, v)
+
+    def lock(self):
+        if os.path.exists(self.lock_file):
+            raise Exception('Sorry, lock file!')
+        f = open(self.lock_file, 'w+')
+        f.close()
+
+    def unlock(self):
+        os.remove(self.lock_file)
+
+    def to_dict(self):
+        keys = [k for k in dir(self) if (not k.startswith('_')) and (not callable(getattr(self, k)))]
+        d = {}
+        for k in keys:
+            d[k] = getattr(self, k)
+        return d
+
+    def write_db(self):
+        self.write_text_to_db(json.dumps(self.to_dict()))
+
+    def backup(self):
+        import shutil
+        from datetime import datetime, timedelta
+        # collect modification times of numbered .bak files
+        bak_prefix = f'{self.db_file}.bak.'
+        backup_dates = []
+        i = 0
+        bak_as = f'{bak_prefix}{i}'
+        while os.path.exists(bak_as):
+            mod_time = os.path.getmtime(bak_as)
+            backup_dates += [str(datetime.fromtimestamp(mod_time))]
+            i += 1
+            bak_as = f'{bak_prefix}{i}'
+
+        # collect what numbered .bak files to save: the older, the fewer; for each
+        # timedelta, keep the newest file that's older
+        ages_to_keep = [timedelta(minutes=4**i) for i in range(0, 8)]
+        now = datetime.now() 
+        to_save = []
+        for age in ages_to_keep:
+            limit = now - age 
+            for i, date in enumerate(reversed(backup_dates)):
+                if datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f') < limit:
+                    unreversed_i = len(backup_dates) - i - 1
+                    if unreversed_i not in to_save:
+                        to_save += [unreversed_i]
+                    break
+
+        # remove redundant backup files 
+        j = 0
+        for i in to_save:
+            if i != j:
+                source = f'{bak_prefix}{i}'
+                target = f'{bak_prefix}{j}'
+                shutil.move(source, target)
+            j += 1
+        for i in range(j, len(backup_dates)):
+            try:
+                os.remove(f'{bak_prefix}{i}')
+            except FileNotFoundError:
+                pass
+
+        # put copy of current state at end of bak list 
+        shutil.copy(self.db_file, f'{bak_prefix}{j}')
+
+    def write_text_to_db(self, text):
+        self.lock()
+        self.backup()
+        with open(self.db_file, 'w') as f:
+            f.write(text);
+        self.unlock()
+
+class ProgressBar:
+    def __init__(self, title, earned, goal, time_progress=-1):
+        self.title = title
+        self.earned = earned
+        self.time_progress = int(time_progress * 100)
+        success_income = self.earned / goal
+        self.success_income_cut = int(min(success_income, 1.0) * 100)
+        self.success_income_bonus = int(max(success_income - 1.0, 0) * 100)
+        self.success = success_income + 0
+        self.diff_goal = "%.2f€" % (self.earned - goal)
+        if title != "workday":
+            self.diff_goal += "(%.2f€)" % (self.earned - (goal * time_progress))
+        if time_progress >= 0:
+            self.success = 1
+            if time_progress > 0:
+                self.success = success_income / time_progress
 
 class MyServer(BaseHTTPRequestHandler):
 
     def do_POST(self):
         from urllib.parse import parse_qs
         length = int(self.headers['content-length'])
-        postvars = parse_qs(self.rfile.read(length), keep_blank_values=1)
-        db = load_db()
-        db["workday_minutes_worked_1"] = int(postvars[b'workday_minutes_worked_1'][0].decode()) 
-        db["workday_minutes_worked_2"] = int(postvars[b'workday_minutes_worked_2'][0].decode()) 
-        db["workday_minutes_worked_3"] = int(postvars[b'workday_minutes_worked_3'][0].decode()) 
-        db["workday_hourly_rate_1"] = int(postvars[b'workday_hourly_rate_1'][0].decode()) 
-        db["workday_hourly_rate_2"] = int(postvars[b'workday_hourly_rate_2'][0].decode()) 
-        db["workday_hourly_rate_3"] = int(postvars[b'workday_hourly_rate_3'][0].decode()) 
-        db["year_goal"] = int(postvars[b'year_goal'][0].decode()) 
-        db["workdays_per_month"] = int(postvars[b'workdays_per_month'][0].decode()) 
-        if b'finish' in postvars.keys():
-            day_income = (db["workday_minutes_worked_1"] / 60.0) * db["workday_hourly_rate_1"] 
-            day_income += (db["workday_minutes_worked_2"] / 60.0) * db["workday_hourly_rate_2"] 
-            day_income += (db["workday_minutes_worked_3"] / 60.0) * db["workday_hourly_rate_3"] 
-            db["year_income"] += day_income 
-            db["month_income"] += day_income 
-            db["week_income"] += day_income 
-            db["workday_minutes_worked_1"] = 0
-            db["workday_minutes_worked_2"] = 0
-            db["workday_minutes_worked_3"] = 0
-        if not os.path.exists(lock_file):
-            with open(lock_file, "w+"): pass
-            with open(db_file, "w") as f:
-                json.dump(db, f)
-            os.remove(lock_file)
-            self.send_response(302)
-            self.send_header('Location', '/')
-            self.end_headers()
-        else:
-            self.send_response(400)
-            self.end_headers()
-            self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
+        postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
+        db = Database()
+        db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0])
+        db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0])
+        db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0])
+        db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0])
+        db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0])
+        db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0])
+        db.year_goal = int(postvars['year_goal'][0])
+        db.workdays_per_month = int(postvars['workdays_per_month'][0])
+        if 'finish' in postvars.keys():
+            day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
+            day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
+            day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
+            db.year_income += day_income
+            db.month_income += day_income
+            db.week_income += day_income
+            db.workday_minutes_worked_1 = 0
+            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()
 
     def do_GET(self):
         import datetime
-        import calendar 
+        import calendar
+        db = Database() #load_db()
+        today = datetime.datetime.now()
+        update_db = False
+        if today.year != db.timestamp_year:
+            db.timestamp_year = today.year
+            db.timestamp_month = today.month
+            db.year_income = 0
+            db.month_income = 0
+            update_db = True
+        if today.month != db.timestamp_month:
+            db.timestamp_month = today.month
+            db.month_income = 0
+            update_db = True
+        if today.isocalendar()[1] != db.timestamp_week:
+            db.timestamp_week = today.isocalendar()[1]
+            db.week_income = 0
+            update_db = True
+        if update_db:
+            print("Resetting timestamp")
+            db.write_db()
+        day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
+        year_length = 365 + calendar.isleap(today.year)
+        workday_goal = db.year_goal / 12 / db.workdays_per_month
+        workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7)
+        month_goal = db.year_goal / 12
+        week_goal = db.year_goal / (year_length / 7)
+        day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
+        day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
+        day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
+        year_plus = db.year_income + day_income
+        month_plus = db.month_income + day_income
+        week_plus = db.week_income + day_income
+        progress_time_year = day_of_year / year_length
+        progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1]
+        progress_time_week = today.weekday() / 7
+        progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year),
+                         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(
+                progress_bars = progress_bars,
+                workday_hourly_rate_1 = db.workday_hourly_rate_1,
+                workday_minutes_worked_1 = db.workday_minutes_worked_1,
+                workday_hourly_rate_2 = db.workday_hourly_rate_2,
+                workday_minutes_worked_2 = db.workday_minutes_worked_2,
+                workday_hourly_rate_3 = db.workday_hourly_rate_3,
+                workday_minutes_worked_3 = db.workday_minutes_worked_3,
+                year_goal = db.year_goal,
+                month_goal = month_goal,
+                week_goal = week_goal,
+                workdays_per_month = db.workdays_per_month,
+                workday_goal = workday_goal,
+                workdays_per_week = workdays_per_week,
+                )
         self.send_response(200)
         self.send_header("Content-type", "text/html")
         self.end_headers()
-        db = load_db()
-        today = datetime.datetime.now()
-        if not os.path.exists(lock_file):
-            update_db = False
-            if today.year != db["timestamp_year"]:
-                db["timestamp_year"] = today.year
-                db["year_income"] = 0
-                update_db = True 
-            if today.month != db["timestamp_month"]:
-                db["timestamp_month"] = today.month
-                db["month_income"] = 0
-                update_db = True 
-            if today.isocalendar()[1] != db["timestamp_week"]:
-                db["timestamp_week"] = today.isocalendar()[1]
-                db["week_income"] = 0
-                update_db = True 
-            if update_db:
-                print("Resetting timestamp")
-                with open(lock_file, "w+"): pass
-                with open(db_file, "w") as f:
-                    json.dump(db, f)
-                os.remove(lock_file)
-        else:
+        self.wfile.write(bytes(page, "utf-8"))
+
+    def fail_on_lockfile(self):
+        if os.path.exists(lock_file):
             self.send_response(400)
             self.end_headers()
             self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
-            return
-        day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
-        year_length = 365 + calendar.isleap(today.year)
-        workday_goal = db["year_goal"] / 12 / db["workdays_per_month"] 
-        workdays_per_week = (db["workdays_per_month"] * 12) / (year_length / 7) 
-        month_goal = db["year_goal"] / 12 
-        week_goal = db["year_goal"] / (year_length / 7) 
-        def success_color(success):
-            if success < 0.5:
-                return "red";
-            elif success < 1:
-                return "yellow";
-            else: 
-                return "green"
-        def progressbar(title, earned, goal, time_progress=-1):
-            time_progress_indicator = ""
-            success_income = earned / goal 
-            success_income_cut = min(success_income, 1.0) 
-            success_income_bonus = max(success_income - 1.0, 0) 
-            success = success_income + 0
-            diff_goal = "%.2f€" % (earned - goal)
-            if title != "workday":
-                diff_goal += "(%.2f€)" % (earned - (goal * time_progress))
-            if time_progress >= 0:
-                success = 1
-                if time_progress > 0:
-                    success = success_income / time_progress
-                time_progress_indicator = "<div class=\"time_progress\" style=\"margin-left: %spx\"></div>" % int(time_progress * 100) 
-            return "<tr><th>%s</th>" \
-                    "<td class=\"countable\">%.2f€</td>" \
-                    "<td class=\"progressbar\">%s<div class=\"progress\" style=\"background-color: %s; width: %s\"></div></td>" \
-                    "<td class=\"progressbar surplusbar\"><div class=\"diff_goal\">%s</div><div class=\"progress surplus\" style=\"width: %s\"></div></td></tr>" % (
-                    title, earned, time_progress_indicator, success_color(success), int(success_income_cut * 100), diff_goal, int(success_income_bonus * 100))
-        day_income = (db["workday_minutes_worked_1"] / 60.0) * db["workday_hourly_rate_1"] 
-        day_income += (db["workday_minutes_worked_2"] / 60.0) * db["workday_hourly_rate_2"] 
-        day_income += (db["workday_minutes_worked_3"] / 60.0) * db["workday_hourly_rate_3"] 
-        year_plus = db["year_income"] + day_income 
-        month_plus = db["month_income"] + day_income 
-        week_plus = db["week_income"] + day_income
-        progress_time_year = day_of_year / year_length 
-        progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1] 
-        progress_time_week = today.weekday() / 7 
-        year_line = progressbar("year", year_plus, db["year_goal"], progress_time_year)
-        month_line = progressbar("month", month_plus, month_goal, progress_time_month)
-        week_line = progressbar("week", week_plus, week_goal, progress_time_week)
-        day_line = progressbar("workday", day_income, workday_goal)
-        body = year_line + "\n" + month_line + "\n" + week_line + "\n" + day_line
-        page = header + body + footer % (
-                db["workday_hourly_rate_1"], db["workday_minutes_worked_1"],
-                db["workday_hourly_rate_2"], db["workday_minutes_worked_2"],
-                db["workday_hourly_rate_3"], db["workday_minutes_worked_3"],
-                db["year_goal"],
-                month_goal,
-                week_goal,
-                db["workdays_per_month"],
-                workday_goal,
-                workdays_per_week,
-                )
-        self.wfile.write(bytes(page, "utf-8"))
+            return True
+        return False
 
-if __name__ == "__main__":        
+if __name__ == "__main__":       
     webServer = HTTPServer((hostName, serverPort), MyServer)
-    print("Server started http://%s:%s" % (hostName, serverPort))
+    print(f"Server started http://{hostName}:{serverPort}")
     try:
         webServer.serve_forever()
     except KeyboardInterrupt: