source: stamper/stamper/stamper.py@ 23:bc0b04a989aa

Last change on this file since 23:bc0b04a989aa was 23:bc0b04a989aa, checked in by Óscar M. Lage <info@…>, 10 years ago

Graphs, first version (limited to 15 days, without customer the graph order is inverse)

File size: 9.6 KB
Line 
1
2import json
3import re
4import pygal
5from datetime import datetime, timedelta
6from os.path import expanduser, exists
7from collections import OrderedDict
8
9
10STAMPS_FILE = expanduser('~/.workstamps.json')
11DATE_FORMAT = '%Y-%m-%d %H:%M'
12HOURS_DAY = 8
13SECS_DAY = HOURS_DAY * 60 * 60
14
15
16class Stamper(object):
17
18 def __init__(self, stamps_file=STAMPS_FILE):
19 self.stamps_file = STAMPS_FILE
20 self.ensure_stamps_file()
21 self.stamps = []
22
23 def ensure_stamps_file(self):
24 if not exists(self.stamps_file):
25 with open(self.stamps_file, 'w') as stamps_file:
26 stamps_file.write('')
27
28 def load_stamps(self):
29 with open(self.stamps_file, 'r') as stamps_file:
30 try:
31 self.stamps = json.load(stamps_file)
32 except ValueError:
33 self.stamps = []
34
35 def save_stamps(self):
36 with open(self.stamps_file, 'w') as stamps_file:
37 json.dump(self.stamps, stamps_file, indent=4)
38
39 def stamp(self, start, end, customer, action):
40 self.stamps.append({
41 'start': start,
42 'end': end,
43 'customer': customer,
44 'action': action,
45 })
46
47 def last_stamp(self):
48 if not self.stamps:
49 return None
50 return self.stamps[-1]
51
52 def worktime(self, start, end):
53 worktime = (datetime.strptime(end, DATE_FORMAT) -
54 datetime.strptime(start, DATE_FORMAT))
55 return worktime.seconds
56
57 def validate_filter(self, stamp_filter):
58 """
59 Validate a given filter. Filters can have the following notation:
60
61 - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates
62
63 - -%Y-%m-%d: Times recorded before a given date
64
65 - +%Y-%m-%d: Times recorded after a given date
66
67 - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
68 """
69 # First try the date filters, one by one
70 matches = ['%Y-%m-%d', '-%Y-%m-%d', '+%Y-%m-%d']
71 for match in matches:
72 try:
73 if '--' in stamp_filter:
74 filter_from, filter_to = stamp_filter.split('--')
75 filter_from = datetime.strptime(stamp_filter, match)
76 filter_to = datetime.strptime(stamp_filter, match)
77 else:
78 valid_filter = datetime.strptime(stamp_filter, match)
79 except ValueError:
80 pass
81 else:
82 return stamp_filter
83
84 valid_filter = re.search(r'(\d+[dwmyDWMY]{1})', stamp_filter)
85 if valid_filter:
86 return stamp_filter
87
88 # Not a valid filter
89 return None
90
91 def customers(self):
92 customers = []
93 for stamp in self.stamps:
94 if stamp['customer'] not in customers:
95 customers.append(stamp['customer'])
96 customers.remove(None)
97 return customers
98
99 def totals(self, stamp_filter=None):
100 totals = {}
101 for stamp in self.stamps:
102 customer = stamp['customer']
103 if customer:
104 # c will be None for "start" stamps, having no end time
105 if customer not in totals:
106 totals[customer] = 0
107 totals[customer] += self.worktime(stamp['start'], stamp['end'])
108 return totals
109
110 def details(self):
111 details = OrderedDict()
112 totals = OrderedDict()
113 total_customer = OrderedDict()
114 for stamp in self.stamps:
115 if stamp['customer']:
116 # avoid "start" stamps
117 start_day = stamp['start'].split(' ')[0]
118 if start_day not in details:
119 details[start_day] = []
120 if start_day not in totals:
121 totals[start_day] = 0
122 worktime = self.worktime(stamp['start'], stamp['end'])
123 details[start_day].append(
124 ' -> %(worktime)s %(customer)s %(action)s' % {
125 'worktime': str(timedelta(seconds=worktime)),
126 'customer': stamp['customer'],
127 'action': stamp['action']
128 })
129 customer = stamp['customer']
130 totals[start_day] += worktime
131 if start_day not in total_customer:
132 total_customer[start_day] = {}
133 if customer not in total_customer[start_day]:
134 total_customer[start_day][customer] = 0
135 total_customer[start_day][customer] += worktime
136 for day in totals:
137 totals[day] = str(timedelta(seconds=totals[day]))
138 return details, totals, total_customer
139
140 def details_by_customer(self, customer):
141 details = OrderedDict()
142 totals = OrderedDict()
143 for stamp in self.stamps:
144 if stamp['customer'] == customer:
145 start_day = stamp['start'].split(' ')[0]
146 if start_day not in details:
147 details[start_day] = []
148 if start_day not in totals:
149 totals[start_day] = 0
150 worktime = self.worktime(stamp['start'], stamp['end'])
151 details[start_day].append(
152 ' -> %(worktime)s %(customer)s %(action)s' % {
153 'worktime': str(timedelta(seconds=worktime)),
154 'customer': stamp['customer'],
155 'action': stamp['action']
156 })
157 totals[start_day] += worktime
158 for day in totals:
159 totals[day] = str(timedelta(seconds=totals[day]))
160 return details, totals
161
162 def show_stamps(self, customer=None, stamp_filter=None, verbose=False,
163 sum=False, graph=False):
164 if stamp_filter:
165 stamp_filter = self.validate_filter(stamp_filter)
166
167 totals = self.totals(stamp_filter)
168
169 if customer:
170 seconds=totals.get(customer, 0)
171 total = timedelta(seconds=totals.get(customer, 0))
172 print(' %(customer)s: %(total)s' % {'customer': customer,
173 'total': total})
174 else:
175 for c in totals:
176 seconds=totals[c]
177 total = timedelta(seconds=totals[c])
178 print(' %(customer)s: %(total)s' % {'customer': c,
179 'total': total})
180
181 if verbose:
182 if customer:
183 details, totals = self.details_by_customer(customer)
184 else:
185 details, totals, total_customer = self.details()
186 for day in details:
187 print('------ %(day)s ------' % {'day': day})
188 for line in details[day]:
189 print(line)
190 print(' Total: %(total)s' % {'total': totals[day]})
191
192 if sum:
193 sum_tot = ''
194 if totals:
195 print('------ Totals ------' % {'day': day})
196 for day, tot in totals.iteritems():
197 print(' %(day)s: %(total)s' % {'day': day, 'total': tot})
198 sum_tot = "%(total)s %(new)s" % {
199 'total': sum_tot,
200 'new': total
201 }
202 totalSecs, sec = divmod(seconds, 60)
203 hr, min = divmod(totalSecs, 60)
204 totalDays, remaining = divmod(seconds, SECS_DAY)
205 remainingMin, remainingSec = divmod(remaining, (60))
206 remainingHr, remainingMin = divmod(remainingMin, (60))
207 print('----- %d:%02d:%02d -----' % (hr, min, sec))
208 print('--- %d days, remaining: %d:%02d (%d hours/day) ---' % (
209 totalDays, remainingHr, remainingMin, HOURS_DAY
210 ))
211
212 if graph:
213 DAYS = 15
214 list_days = []
215 list_tot = []
216 stackedbar_chart = pygal.StackedBar()
217 stackedbar_chart.title = 'Worked time per day (in hours)'
218
219 if customer:
220 for day, tot in totals.iteritems():
221 list_days.append(day)
222 (h, m, s) = tot.split(':')
223 tot_sec = int(h) * 3600 + int(m) * 60 + int(s)
224 tot_h = float(tot_sec / float(60) / float(60))
225 list_tot.append(tot_h)
226 stackedbar_chart.add(customer, list_tot)
227 stackedbar_chart.x_labels = map(str, list_days)
228 stackedbar_chart.render_to_file('graphs/chart-%s.svg' % customer )
229 else:
230 all_customers = self.customers()
231 total_per_customer = {}
232 details, totals, total_customer = self.details()
233 chars = 0
234 total_customer_reverse = total_customer.items()
235 total_customer_reverse.reverse()
236 for day, tot in total_customer_reverse:
237 if chars < DAYS:
238 list_days.append(day)
239 for cust in self.customers():
240 if cust not in tot:
241 tot[cust] = 0
242 for cus, time in tot.iteritems():
243 tot_h = float(time / float(60) / float(60))
244 if cus not in total_per_customer:
245 total_per_customer[cus] = []
246 total_per_customer[cus].append(tot_h)
247 chars = chars + 1
248 for ccus, ctime in total_per_customer.iteritems():
249 stackedbar_chart.add(ccus, ctime)
250 stackedbar_chart.x_labels = map(str, list_days[-DAYS:])
251 stackedbar_chart.render_to_file('graphs/chart-all.svg')
Note: See TracBrowser for help on using the repository browser.