home · contact · privacy
Add foreign key restraints, expand and fix tests, add deletion and forking.
[misc] / income_progress_bars.py
1 import os
2 import json
3 import jinja2
4 from plomlib import PlomDB, PlomException, run_server, PlomHandler 
5
6 server_port = 8083
7 db_path = '/home/plom/org/income.json'
8
9 tmpl = """
10 <style>
11 body {
12   font-family: monospace;
13   background-color: #e0e0ff;
14 }
15 .countable {
16   font-family: monospace;
17   text-align: right;
18 }
19 td, th {
20   border: 1px solid black;
21 }
22 .progressbar {
23   width: 100px;
24   height: 20px;
25   background-color: black;
26   border: none;
27 }
28 .surplusbar {
29   width: 200px;
30 }
31 .time_progress {
32   position: absolute;
33   height: 20px;
34   background-color: white;
35   width: 2px;
36   border-left: 1px solid black;
37   border-right: 1px solid black;
38   z-index: 2;
39 }
40 .progress {
41   height: 20px;
42   z-index: 1;
43 }
44 .surplus {
45   background-color: green;
46 }
47 input {
48   font-family: monospace;
49   text-align: right;
50 }
51 input.rate {
52   text-align: center;
53   width: 4em;
54 }
55 input.minutes {
56   width: 4em;
57 }
58 input.workdays {
59   width: 3em;
60 }
61 input.year_goal {
62   width: 5em;
63 }
64 .diff_goal {
65   position: absolute;
66   width: 7em;
67   text-align: right;
68   color: white;
69   z-index: 3;
70 }
71 table {
72   margin-bottom: 2em;
73 }
74 .input_container {
75   text-align: center;
76 }
77 </style>
78 <body>
79 <table>
80 <tr><th></th><th>earned</th><th>progress</th><th>surplus</th></tr >
81 {% for p in progress_bars %}<tr><th>{{p.title}}</th>
82 <td class="countable">{{p.earned|round(2)}}</td>
83 <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>
84 <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>
85 {% endfor %}</table>
86
87 <form action="{{homepage}}" method="POST">
88 <table>
89 <tr><th>hourly rate</th><th>worked today</th></tr>
90 <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>
91 <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>
92 <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>
93 </table>
94 <table>
95 <tr><th>yearly income goal</th><td><input type="number" class="year_goal" min="1" name="year_goal" value="{{year_goal}}" />€</td></tr>
96 <tr><th>monthly income goal</th><td class="countable">{{month_goal|round(2)}}€</td></tr>
97 <tr><th>weekly income goal</th><td class="countable">{{week_goal|round(2)}}€</td></tr>
98 <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>
99 <tr><th>workday income goal</th><td class="countable">{{workday_goal|round(2)}}€</td></tr>
100 <tr><th>workdays per week</th><td class="countable">{{workdays_per_week|round(2)}}</td></tr>
101 </table>
102 <input type="submit" name="update" value="update inputs" />
103 <input type="submit" name="finish" value="finish day" />
104 </form>
105 """
106
107
108 class IncomeDB(PlomDB):
109
110     def __init__(self):
111         # defaults
112         self.timestamp_year = 0,
113         self.timestamp_month = 0,
114         self.timestamp_week = 0,
115         self.year_income = 0,
116         self.month_income = 0,
117         self.week_income = 0,
118         self.workday_hourly_rate_1 = 10,
119         self.workday_hourly_rate_2 = 25,
120         self.workday_hourly_rate_3 = 50,
121         self.workday_minutes_worked_1 = 0,
122         self.workday_minutes_worked_2 = 0,
123         self.workday_minutes_worked_3 = 0,
124         self.year_goal = 20000,
125         self.workdays_per_month = 16
126         super().__init__(db_path)
127
128     def read_db_file(self, f):
129         d = json.load(f)
130         for k, v in d.items():
131             if not hasattr(self, k):
132                 raise PlomException("bad key in db: " + k)
133             setattr(self, k, v)
134
135     def to_dict(self):
136         keys = [k for k in dir(self) if (not k.startswith('_')) and (not callable(getattr(self, k)))]
137         d = {}
138         for k in keys:
139             d[k] = getattr(self, k)
140         return d
141
142     def write_db(self):
143         self.write_text_to_db(json.dumps(self.to_dict()))
144
145
146 class ProgressBar:
147     def __init__(self, title, earned, goal, time_progress=-1):
148         self.title = title
149         self.earned = earned
150         self.time_progress = int(time_progress * 100)
151         success_income = self.earned / goal
152         self.success_income_cut = int(min(success_income, 1.0) * 100)
153         self.success_income_bonus = int(max(success_income - 1.0, 0) * 100)
154         self.success = success_income + 0
155         self.diff_goal = "%.2f€" % (self.earned - goal)
156         if title != "workday":
157             self.diff_goal += "(%.2f€)" % (self.earned - (goal * time_progress))
158         if time_progress >= 0:
159             self.success = 1
160             if time_progress > 0:
161                 self.success = success_income / time_progress
162
163
164 class IncomeProgressHandler(PlomHandler):
165     
166     def app_init(self, handler):
167         default_path = '/income_progress'
168         handler.add_route('GET', default_path, self.display_income_progress) 
169         handler.add_route('POST', default_path, self.post_income_update) 
170         return 'income_progress', default_path 
171
172     def do_POST(self):
173         self.try_do(self.post_income_update)
174
175     def post_income_update(self):
176         from urllib.parse import parse_qs
177         length = int(self.headers['content-length'])
178         postvars = parse_qs(self.rfile.read(length).decode(), keep_blank_values=1)
179         db = IncomeDB()
180         db.workday_minutes_worked_1 = int(postvars['workday_minutes_worked_1'][0])
181         db.workday_minutes_worked_2 = int(postvars['workday_minutes_worked_2'][0])
182         db.workday_minutes_worked_3 = int(postvars['workday_minutes_worked_3'][0])
183         db.workday_hourly_rate_1 = int(postvars['workday_hourly_rate_1'][0])
184         db.workday_hourly_rate_2 = int(postvars['workday_hourly_rate_2'][0])
185         db.workday_hourly_rate_3 = int(postvars['workday_hourly_rate_3'][0])
186         db.year_goal = int(postvars['year_goal'][0])
187         db.workdays_per_month = int(postvars['workdays_per_month'][0])
188         if 'finish' in postvars.keys():
189             day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
190             day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
191             day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
192             db.year_income += day_income
193             db.month_income += day_income
194             db.week_income += day_income
195             db.workday_minutes_worked_1 = 0
196             db.workday_minutes_worked_2 = 0
197             db.workday_minutes_worked_3 = 0
198         db.write_db()
199         homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage
200         self.redirect(homepage)
201
202     def do_GET(self):
203         self.try_do(self.display_income_progress)
204
205     def display_income_progress(self):
206         import datetime
207         import calendar
208         db = IncomeDB()
209         today = datetime.datetime.now()
210         update_db = False
211         if today.year != db.timestamp_year:
212             db.timestamp_year = today.year
213             db.timestamp_month = today.month
214             db.year_income = 0
215             db.month_income = 0
216             update_db = True
217         if today.month != db.timestamp_month:
218             db.timestamp_month = today.month
219             db.month_income = 0
220             update_db = True
221         if today.isocalendar()[1] != db.timestamp_week:
222             db.timestamp_week = today.isocalendar()[1]
223             db.week_income = 0
224             update_db = True
225         if update_db:
226             print("Resetting timestamp")
227             db.write_db()
228         day_of_year = today.toordinal() - datetime.date(today.year, 1, 1).toordinal() + 1
229         year_length = 365 + calendar.isleap(today.year)
230         workday_goal = db.year_goal / 12 / db.workdays_per_month
231         workdays_per_week = (db.workdays_per_month * 12) / (year_length / 7)
232         month_goal = db.year_goal / 12
233         week_goal = db.year_goal / (year_length / 7)
234         day_income = (db.workday_minutes_worked_1 / 60.0) * db.workday_hourly_rate_1
235         day_income += (db.workday_minutes_worked_2 / 60.0) * db.workday_hourly_rate_2
236         day_income += (db.workday_minutes_worked_3 / 60.0) * db.workday_hourly_rate_3
237         year_plus = db.year_income + day_income
238         month_plus = db.month_income + day_income
239         week_plus = db.week_income + day_income
240         progress_time_year = day_of_year / year_length
241         progress_time_month = today.day / calendar.monthrange(today.year, today.month)[1]
242         progress_time_week = today.weekday() / 7
243         progress_bars = [ProgressBar("year", year_plus, db.year_goal, progress_time_year),
244                          ProgressBar("month", month_plus, month_goal, progress_time_month),
245                          ProgressBar("week", week_plus, week_goal, progress_time_week),
246                          ProgressBar("workday", day_income, workday_goal)]
247         homepage = self.apps['income_progress'] if hasattr(self, 'apps') else self.homepage
248         page = jinja2.Template(tmpl).render(
249                 homepage = homepage,
250                 progress_bars = progress_bars,
251                 workday_hourly_rate_1 = db.workday_hourly_rate_1,
252                 workday_minutes_worked_1 = db.workday_minutes_worked_1,
253                 workday_hourly_rate_2 = db.workday_hourly_rate_2,
254                 workday_minutes_worked_2 = db.workday_minutes_worked_2,
255                 workday_hourly_rate_3 = db.workday_hourly_rate_3,
256                 workday_minutes_worked_3 = db.workday_minutes_worked_3,
257                 year_goal = db.year_goal,
258                 month_goal = month_goal,
259                 week_goal = week_goal,
260                 workdays_per_month = db.workdays_per_month,
261                 workday_goal = workday_goal,
262                 workdays_per_week = workdays_per_week,
263                 )
264         self.send_HTML(page)
265
266
267 if __name__ == "__main__":       
268     run_server(server_port, IncomeProgressHandler)