home · contact · privacy
Improve income_progress_bars.py.
[misc] / income_progress_bars.py
1 from http.server import BaseHTTPRequestHandler, HTTPServer
2 import os
3 import json
4 import jinja2
5
6 hostName = "localhost"
7 serverPort = 8081
8
9 tmpl = jinja2.Template("""<html>
10 <meta charset="UTF-8">
11 <style>
12 body {
13   font-family: monospace;
14   background-color: #e0e0ff;
15 }
16 .countable {
17   font-family: monospace;
18   text-align: right;
19 }
20 td, th {
21   border: 1px solid black;
22 }
23 .progressbar {
24   width: 100px;
25   height: 20px;
26   background-color: black;
27   border: none;
28 }
29 .surplusbar {
30   width: 200px;
31 }
32 .time_progress {
33   position: absolute;
34   height: 20px;
35   background-color: white;
36   width: 2px;
37   border-left: 1px solid black;
38   border-right: 1px solid black;
39   z-index: 2;
40 }
41 .progress {
42   height: 20px;
43   z-index: 1;
44 }
45 .surplus {
46   background-color: green;
47 }
48 input {
49   font-family: monospace;
50   text-align: right;
51 }
52 input.rate {
53   text-align: center;
54   width: 4em;
55 }
56 input.minutes {
57   width: 4em;
58 }
59 input.workdays {
60   width: 3em;
61 }
62 input.year_goal {
63   width: 5em;
64 }
65 .diff_goal {
66   position: absolute;
67   width: 7em;
68   text-align: right;
69   color: white;
70   z-index: 3;
71 }
72 table {
73   margin-bottom: 2em;
74 }
75 .input_container {
76   text-align: center;
77 }
78 </style>
79 <body>
80 <table>
81 <tr><th /><th>earned</th><th>progress</th><th>surplus</th></tr >
82 {% for p in progress_bars %}
83 <tr><th>{{p.title}}</th>
84 <td class="countable">{{p.earned|round(2)}}</td>
85 <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>
86 <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>
87 {% endfor %}
88 </table>
89 <form action="/" method="POST">
90 <table>
91 <tr><th>hourly rate</th><th>worked today</th></tr>
92 <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>
93 <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>
94 <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>
95 <table>
96 <tr><th>yearly income goal</th><td><input type="number" class="year_goal" min="1" name="year_goal" value="{{year_goal}}" />€</td></tr>
97 <tr><th>monthly income goal</th><td class="countable">{{month_goal|round(2)}}€</td></tr>
98 <tr><th>weekly income goal</th><td class="countable">{{week_goal|round(2)}}€</td></tr>
99 <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>
100 <tr><th>workday income goal</th><td class="countable">{{workday_goal|round(2)}}€</td></tr>
101 <tr><th>workdays per week</th><td class="countable">{{workdays_per_week|round(2)}}</td></tr>
102 </table>
103 <input type="submit" name="update" value="update inputs" />
104 <input type="submit" name="finish" value="finish day" />
105 </form>
106 </body
107 </html>""")
108
109 db_default = {
110   "timestamp_year": 0,
111   "timestamp_month": 0,
112   "timestamp_week": 0,
113   "year_income": 0,
114   "month_income": 0,
115   "week_income": 0,
116   "workday_hourly_rate_1": 10,
117   "workday_minutes_worked_1": 0,
118   "workday_hourly_rate_2": 25,
119   "workday_minutes_worked_2": 0,
120   "workday_hourly_rate_3": 50,
121   "workday_minutes_worked_3": 0,
122   "year_goal": 20000,
123   "workdays_per_month": 16
124 }
125 db_file = "db.json"
126 lock_file = "db.lock"
127 def load_db():
128     if os.path.exists(db_file):
129         with open(db_file, "r") as f:
130             return json.load(f)
131     else:
132         return db_default
133
134 # class Database:
135 #     data_default = {
136 #       "timestamp_year": 0,
137 #       "timestamp_month": 0,
138 #       "timestamp_week": 0,
139 #       "year_income": 0,
140 #       "month_income": 0,
141 #       "week_income": 0,
142 #       "workday_hourly_rate_1": 10,
143 #       "workday_minutes_worked_1": 0,
144 #       "workday_hourly_rate_2": 25,
145 #       "workday_minutes_worked_2": 0,
146 #       "workday_hourly_rate_3": 50,
147 #       "workday_minutes_worked_3": 0,
148 #       "year_goal": 20000,
149 #       "workdays_per_month": 16
150 #     }
151 #     def __init__(self):
152 #         if os.path.exists(db_file):
153 #             with open(db_file, "r") as f:
154 #                 return json.load(f)
155 #         else:
156 #             return db_default
157
158 class ProgressBar:
159     def __init__(self, title, earned, goal, time_progress=-1):
160         self.title = title
161         self.earned = earned
162         self.time_progress = int(time_progress * 100)
163         success_income = self.earned / goal
164         self.success_income_cut = int(min(success_income, 1.0) * 100)
165         self.success_income_bonus = int(max(success_income - 1.0, 0) * 100)
166         self.success = success_income + 0
167         self.diff_goal = "%.2f€" % (self.earned - goal)
168         if title != "workday":
169             self.diff_goal += "(%.2f€)" % (self.earned - (goal * time_progress))
170         if time_progress >= 0:
171             self.success = 1
172             if time_progress > 0:
173                 self.success = success_income / time_progress
174
175 class MyServer(BaseHTTPRequestHandler):
176
177     def do_POST(self):
178         from urllib.parse import parse_qs
179         length = int(self.headers['content-length'])
180         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
181         db = load_db()
182         db["workday_minutes_worked_1"] = int(postvars['workday_minutes_worked_1'][0])
183         db["workday_minutes_worked_2"] = int(postvars['workday_minutes_worked_2'][0])
184         db["workday_minutes_worked_3"] = int(postvars['workday_minutes_worked_3'][0])
185         db["workday_hourly_rate_1"] = int(postvars['workday_hourly_rate_1'][0])
186         db["workday_hourly_rate_2"] = int(postvars['workday_hourly_rate_2'][0])
187         db["workday_hourly_rate_3"] = int(postvars['workday_hourly_rate_3'][0])
188         db["year_goal"] = int(postvars['year_goal'][0])
189         db["workdays_per_month"] = int(postvars['workdays_per_month'][0])
190         if b'finish' in postvars.keys():
191             day_income = (db["workday_minutes_worked_1"] / 60.0) * db["workday_hourly_rate_1"]
192             day_income += (db["workday_minutes_worked_2"] / 60.0) * db["workday_hourly_rate_2"]
193             day_income += (db["workday_minutes_worked_3"] / 60.0) * db["workday_hourly_rate_3"]
194             db["year_income"] += day_income
195             db["month_income"] += day_income
196             db["week_income"] += day_income
197             db["workday_minutes_worked_1"] = 0
198             db["workday_minutes_worked_2"] = 0
199             db["workday_minutes_worked_3"] = 0
200         if self.fail_on_lockfile():
201             return
202         with open(lock_file, "w+"): pass
203         with open(db_file, "w") as f:
204             json.dump(db, f)
205         os.remove(lock_file)
206         self.send_response(302)
207         self.send_header('Location', '/')
208         self.end_headers()
209
210     def do_GET(self):
211         import datetime
212         import calendar
213         db = load_db()
214         today = datetime.datetime.now()
215         update_db = False
216         if today.year != db["timestamp_year"]:
217             db["timestamp_year"] = today.year
218             db["year_income"] = 0
219             update_db = True
220         if today.month != db["timestamp_month"]:
221             db["timestamp_month"] = today.month
222             db["month_income"] = 0
223             update_db = True
224         if today.isocalendar()[1] != db["timestamp_week"]:
225             db["timestamp_week"] = today.isocalendar()[1]
226             db["week_income"] = 0
227             update_db = True
228         if self.fail_on_lockfile():
229             return
230         if update_db:
231             print("Resetting timestamp")
232             with open(lock_file, "w+"): pass
233             with open(db_file, "w") as f:
234                 json.dump(db, f)
235             os.remove(lock_file)
236         day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
237         year_length = 365 + calendar.isleap(today.year)
238         workday_goal = db["year_goal"] / 12 / db["workdays_per_month"]
239         workdays_per_week = (db["workdays_per_month"] * 12) / (year_length / 7)
240         month_goal = db["year_goal"] / 12
241         week_goal = db["year_goal"] / (year_length / 7)
242         day_income = (db["workday_minutes_worked_1"] / 60.0) * db["workday_hourly_rate_1"]
243         day_income += (db["workday_minutes_worked_2"] / 60.0) * db["workday_hourly_rate_2"]
244         day_income += (db["workday_minutes_worked_3"] / 60.0) * db["workday_hourly_rate_3"]
245         year_plus = db["year_income"] + day_income
246         month_plus = db["month_income"] + day_income
247         week_plus = db["week_income"] + day_income
248         progress_time_year = day_of_year / year_length
249         progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1]
250         progress_time_week = today.weekday() / 7
251         progress_bars = [ProgressBar("year", year_plus, db["year_goal"], progress_time_year),
252                          ProgressBar("month", month_plus, month_goal, progress_time_month),
253                          ProgressBar("week", week_plus, week_goal, progress_time_week),
254                          ProgressBar("workday", day_income, workday_goal)]
255         page = tmpl.render(
256                 progress_bars = progress_bars,
257                 workday_hourly_rate_1=db["workday_hourly_rate_1"],
258                 workday_minutes_worked_1=db["workday_minutes_worked_1"],
259                 workday_hourly_rate_2=db["workday_hourly_rate_2"],
260                 workday_minutes_worked_2=db["workday_minutes_worked_2"],
261                 workday_hourly_rate_3=db["workday_hourly_rate_3"],
262                 workday_minutes_worked_3=db["workday_minutes_worked_3"],
263                 year_goal=db["year_goal"],
264                 month_goal=month_goal,
265                 week_goal=week_goal,
266                 workdays_per_month=db["workdays_per_month"],
267                 workday_goal=workday_goal,
268                 workdays_per_week=workdays_per_week,
269                 )
270         self.send_response(200)
271         self.send_header("Content-type", "text/html")
272         self.end_headers()
273         self.wfile.write(bytes(page, "utf-8"))
274
275     def fail_on_lockfile(self):
276         if os.path.exists(lock_file):
277             self.send_response(400)
278             self.end_headers()
279             self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
280             return True
281         return False
282
283 if __name__ == "__main__":       
284     webServer = HTTPServer((hostName, serverPort), MyServer)
285     print(f"Server started http://{hostName}:{serverPort}")
286     try:
287         webServer.serve_forever()
288     except KeyboardInterrupt:
289         pass
290     webServer.server_close()
291     print("Server stopped.")