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 class Database:
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_hourly_rate_2 = 25,
118     workday_hourly_rate_3 = 50,
119     workday_minutes_worked_1 = 0,
120     workday_minutes_worked_2 = 0,
121     workday_minutes_worked_3 = 0,
122     year_goal = 20000,
123     workdays_per_month = 16
124
125     def __init__(self):
126         db_name = "_income"
127         self.db_file = db_name + ".json"
128         self.lock_file = db_name+ ".lock"
129         if os.path.exists(self.db_file):
130             with open(self.db_file, "r") as f:
131                 d = json.load(f)
132                 for k, v in d.items():
133                     if not hasattr(self, k):
134                         raise Exception("bad key in db: " + k)
135                     setattr(self, k, v)
136
137     def lock(self):
138         if os.path.exists(self.lock_file):
139             raise Exception('Sorry, lock file!')
140         f = open(self.lock_file, 'w+')
141         f.close()
142
143     def unlock(self):
144         os.remove(self.lock_file)
145
146     def to_dict(self):
147         keys = [k for k in dir(self) if (not k.startswith('_')) and (not callable(getattr(self, k)))]
148         d = {}
149         for k in keys:
150             d[k] = getattr(self, k)
151         return d
152
153     def write_db(self):
154         self.write_text_to_db(json.dumps(self.to_dict()))
155
156     def backup(self):
157         import shutil
158         from datetime import datetime, timedelta
159         # collect modification times of numbered .bak files
160         bak_prefix = f'{self.db_file}.bak.'
161         backup_dates = []
162         i = 0
163         bak_as = f'{bak_prefix}{i}'
164         while os.path.exists(bak_as):
165             mod_time = os.path.getmtime(bak_as)
166             backup_dates += [str(datetime.fromtimestamp(mod_time))]
167             i += 1
168             bak_as = f'{bak_prefix}{i}'
169
170         # collect what numbered .bak files to save: the older, the fewer; for each
171         # timedelta, keep the newest file that's older
172         ages_to_keep = [timedelta(minutes=4**i) for i in range(0, 8)]
173         now = datetime.now() 
174         to_save = []
175         for age in ages_to_keep:
176             limit = now - age 
177             for i, date in enumerate(reversed(backup_dates)):
178                 if datetime.strptime(date, '%Y-%m-%d %H:%M:%S.%f') < limit:
179                     unreversed_i = len(backup_dates) - i - 1
180                     if unreversed_i not in to_save:
181                         to_save += [unreversed_i]
182                     break
183
184         # remove redundant backup files 
185         j = 0
186         for i in to_save:
187             if i != j:
188                 source = f'{bak_prefix}{i}'
189                 target = f'{bak_prefix}{j}'
190                 shutil.move(source, target)
191             j += 1
192         for i in range(j, len(backup_dates)):
193             try:
194                 os.remove(f'{bak_prefix}{i}')
195             except FileNotFoundError:
196                 pass
197
198         # put copy of current state at end of bak list 
199         shutil.copy(self.db_file, f'{bak_prefix}{j}')
200
201     def write_text_to_db(self, text):
202         self.lock()
203         self.backup()
204         with open(self.db_file, 'w') as f:
205             f.write(text);
206         self.unlock()
207
208 class ProgressBar:
209     def __init__(self, title, earned, goal, time_progress=-1):
210         self.title = title
211         self.earned = earned
212         self.time_progress = int(time_progress * 100)
213         success_income = self.earned / goal
214         self.success_income_cut = int(min(success_income, 1.0) * 100)
215         self.success_income_bonus = int(max(success_income - 1.0, 0) * 100)
216         self.success = success_income + 0
217         self.diff_goal = "%.2f€" % (self.earned - goal)
218         if title != "workday":
219             self.diff_goal += "(%.2f€)" % (self.earned - (goal * time_progress))
220         if time_progress >= 0:
221             self.success = 1
222             if time_progress > 0:
223                 self.success = success_income / time_progress
224
225 class MyServer(BaseHTTPRequestHandler):
226
227     def do_POST(self):
228         from urllib.parse import parse_qs
229         length = int(self.headers['content-length'])
230         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
231         db = Database()
232         db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0])
233         db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0])
234         db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0])
235         db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0])
236         db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0])
237         db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0])
238         db.year_goal = int(postvars['year_goal'][0])
239         db.workdays_per_month = int(postvars['workdays_per_month'][0])
240         if 'finish' in postvars.keys():
241             day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
242             day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
243             day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
244             db.year_income += day_income
245             db.month_income += day_income
246             db.week_income += day_income
247             db.workday_minutes_worked_1 = 0
248             db.workday_minutes_worked_2 = 0
249             db.workday_minutes_worked_3 = 0
250         db.write_db()
251         self.send_response(302)
252         self.send_header('Location', '/')
253         self.end_headers()
254
255     def do_GET(self):
256         import datetime
257         import calendar
258         db = Database() #load_db()
259         today = datetime.datetime.now()
260         update_db = False
261         if today.year != db.timestamp_year:
262             db.timestamp_year = today.year
263             db.timestamp_month = today.month
264             db.year_income = 0
265             db.month_income = 0
266             update_db = True
267         if today.month != db.timestamp_month:
268             db.timestamp_month = today.month
269             db.month_income = 0
270             update_db = True
271         if today.isocalendar()[1] != db.timestamp_week:
272             db.timestamp_week = today.isocalendar()[1]
273             db.week_income = 0
274             update_db = True
275         if update_db:
276             print("Resetting timestamp")
277             db.write_db()
278         day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
279         year_length = 365 + calendar.isleap(today.year)
280         workday_goal = db.year_goal / 12 / db.workdays_per_month
281         workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7)
282         month_goal = db.year_goal / 12
283         week_goal = db.year_goal / (year_length / 7)
284         day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
285         day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
286         day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
287         year_plus = db.year_income + day_income
288         month_plus = db.month_income + day_income
289         week_plus = db.week_income + day_income
290         progress_time_year = day_of_year / year_length
291         progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1]
292         progress_time_week = today.weekday() / 7
293         progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year),
294                          ProgressBar("month", month_plus, month_goal, progress_time_month),
295                          ProgressBar("week", week_plus, week_goal, progress_time_week),
296                          ProgressBar("workday", day_income, workday_goal)]
297         page = tmpl.render(
298                 progress_bars = progress_bars,
299                 workday_hourly_rate_1 = db.workday_hourly_rate_1,
300                 workday_minutes_worked_1 = db.workday_minutes_worked_1,
301                 workday_hourly_rate_2 = db.workday_hourly_rate_2,
302                 workday_minutes_worked_2 = db.workday_minutes_worked_2,
303                 workday_hourly_rate_3 = db.workday_hourly_rate_3,
304                 workday_minutes_worked_3 = db.workday_minutes_worked_3,
305                 year_goal = db.year_goal,
306                 month_goal = month_goal,
307                 week_goal = week_goal,
308                 workdays_per_month = db.workdays_per_month,
309                 workday_goal = workday_goal,
310                 workdays_per_week = workdays_per_week,
311                 )
312         self.send_response(200)
313         self.send_header("Content-type", "text/html")
314         self.end_headers()
315         self.wfile.write(bytes(page, "utf-8"))
316
317     def fail_on_lockfile(self):
318         if os.path.exists(lock_file):
319             self.send_response(400)
320             self.end_headers()
321             self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
322             return True
323         return False
324
325 if __name__ == "__main__":       
326     webServer = HTTPServer((hostName, serverPort), MyServer)
327     print(f"Server started http://{hostName}:{serverPort}")
328     try:
329         webServer.serve_forever()
330     except KeyboardInterrupt:
331         pass
332     webServer.server_close()
333     print("Server stopped.")