1 from http.server import BaseHTTPRequestHandler, HTTPServer
9 tmpl = jinja2.Template("""<html>
10 <meta charset="UTF-8">
13 font-family: monospace;
14 background-color: #e0e0ff;
17 font-family: monospace;
21 border: 1px solid black;
26 background-color: black;
35 background-color: white;
37 border-left: 1px solid black;
38 border-right: 1px solid black;
46 background-color: green;
49 font-family: monospace;
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>
89 <form action="/" method="POST">
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>
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>
103 <input type="submit" name="update" value="update inputs" />
104 <input type="submit" name="finish" value="finish day" />
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,
123 workdays_per_month = 16
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:
132 for k, v in d.items():
133 if not hasattr(self, k):
134 raise Exception("bad key in db: " + k)
138 if os.path.exists(self.lock_file):
139 raise Exception('Sorry, lock file!')
140 f = open(self.lock_file, 'w+')
144 os.remove(self.lock_file)
147 keys = [k for k in dir(self) if (not k.startswith('_')) and (not callable(getattr(self, k)))]
150 d[k] = getattr(self, k)
154 self.write_text_to_db(json.dumps(self.to_dict()))
158 from datetime import datetime, timedelta
159 # collect modification times of numbered .bak files
160 bak_prefix = f'{self.db_file}.bak.'
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))]
168 bak_as = f'{bak_prefix}{i}'
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)]
175 for age in ages_to_keep:
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]
184 # remove redundant backup files
188 source = f'{bak_prefix}{i}'
189 target = f'{bak_prefix}{j}'
190 shutil.move(source, target)
192 for i in range(j, len(backup_dates)):
194 os.remove(f'{bak_prefix}{i}')
195 except FileNotFoundError:
198 # put copy of current state at end of bak list
199 shutil.copy(self.db_file, f'{bak_prefix}{j}')
201 def write_text_to_db(self, text):
204 with open(self.db_file, 'w') as f:
209 def __init__(self, title, earned, goal, time_progress=-1):
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:
222 if time_progress > 0:
223 self.success = success_income / time_progress
225 class MyServer(BaseHTTPRequestHandler):
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)
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
251 self.send_response(302)
252 self.send_header('Location', '/')
258 db = Database() #load_db()
259 today = datetime.datetime.now()
261 if today.year != db.timestamp_year:
262 db.timestamp_year = today.year
263 db.timestamp_month = today.month
267 if today.month != db.timestamp_month:
268 db.timestamp_month = today.month
271 if today.isocalendar()[1] != db.timestamp_week:
272 db.timestamp_week = today.isocalendar()[1]
276 print("Resetting timestamp")
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)]
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,
312 self.send_response(200)
313 self.send_header("Content-type", "text/html")
315 self.wfile.write(bytes(page, "utf-8"))
317 def fail_on_lockfile(self):
318 if os.path.exists(lock_file):
319 self.send_response(400)
321 self.wfile.write(bytes("Sorry, lock file!", "utf-8"))
325 if __name__ == "__main__":
326 webServer = HTTPServer((hostName, serverPort), MyServer)
327 print(f"Server started http://{hostName}:{serverPort}")
329 webServer.serve_forever()
330 except KeyboardInterrupt:
332 webServer.server_close()
333 print("Server stopped.")