source: stamper/stamper/stamper.py@ 26:0bcab03222d0

Last change on this file since 26:0bcab03222d0 was 26:0bcab03222d0, checked in by Borja Lopez <borja@…>, 10 years ago

Rewrote the way filters work (initial version, work in progress).
You can learn more about the filters in the docstring of the
validate_filter method. Basically, you can apply the filters
defined there when invoking stamps:

stamps customer 2014-07-01

stamps customer 2014-07-01* -v

stamps customer 2w

Refactored and cleaned up some code (removed the duplicated details
method, details_by_customer)

Moved the print of the details (verbose) before the code that shows
the totals, so we can get a result that looks closer to what the
original stamp tool does.

File size: 12.1 KB
Line 
1
2import json
3import re
4import pygal
5from datetime import datetime, date, timedelta
6from os.path import expanduser, exists
7from collections import OrderedDict
8
9
10STAMPS_FILE = expanduser('~/.workstamps.json')
11DATE_FORMAT = '%Y-%m-%d'
12DATETIME_FORMAT = '%Y-%m-%d %H:%M'
13HOURS_DAY = 8
14SECS_DAY = HOURS_DAY * 60 * 60
15
16
17class Stamper(object):
18
19 def __init__(self, stamps_file=STAMPS_FILE):
20 self.stamps_file = STAMPS_FILE
21 self.ensure_stamps_file()
22 self.stamps = []
23
24 def ensure_stamps_file(self):
25 if not exists(self.stamps_file):
26 with open(self.stamps_file, 'w') as stamps_file:
27 stamps_file.write('')
28
29 def load_stamps(self):
30 with open(self.stamps_file, 'r') as stamps_file:
31 try:
32 self.stamps = json.load(stamps_file)
33 except ValueError:
34 self.stamps = []
35
36 def save_stamps(self):
37 with open(self.stamps_file, 'w') as stamps_file:
38 json.dump(self.stamps, stamps_file, indent=4)
39
40 def stamp(self, start, end, customer, action):
41 self.stamps.append({
42 'start': start,
43 'end': end,
44 'customer': customer,
45 'action': action,
46 })
47
48 def last_stamp(self):
49 if not self.stamps:
50 return None
51 return self.stamps[-1]
52
53 def worktime(self, start, end):
54 worktime = (datetime.strptime(end, DATETIME_FORMAT) -
55 datetime.strptime(start, DATETIME_FORMAT))
56 return worktime.seconds
57
58 def validate_filter(self, stamp_filter):
59 """
60 Validate a given filter. Filters can have the following notation:
61
62 - %Y-%m-%d: Times recorded at a given date
63
64 - %Y-%m-%d--%Y-%m-%d: Times recorded between two dates
65
66 - *%Y-%m-%d: Times recorded up to a given date
67
68 - %Y-%m-%d*: Times recorded from a given date
69
70 - N...N[d|w|m|y]: Times recorded N...N days/weeks/months/years ago
71
72 Important: all date comparisons are made on datetime objects, using
73 00:00 as the time (first second of the given day). This means that
74 for range filters, the first day is included, but the second day is not
75 """
76 filter_from = None
77 filter_to = None
78
79 if stamp_filter is None:
80 return filter_from, filter_to
81
82 if '--' in stamp_filter:
83 filter_from, filter_to = stamp_filter.split('--')
84 filter_from = datetime.strptime(filter_from, DATE_FORMAT)
85 filter_to = datetime.strptime(filter_to, DATE_FORMAT)
86
87 elif stamp_filter.startswith('*'):
88 filter_to = datetime.strptime(stamp_filter, '*'+DATE_FORMAT)
89 filter_to = filter_to.replace(hour=0, minute=0, second=0)
90
91 elif stamp_filter.endswith('*'):
92 filter_from = datetime.strptime(stamp_filter, DATE_FORMAT+'*')
93 filter_from = filter_from.replace(hour=0, minute=0, second=0)
94
95 elif re.search(r'(\d+[dD]{1})', stamp_filter):
96 number = int(stamp_filter.lower().replace('d', ''))
97 delta = timedelta(days=number)
98 filter_from = datetime.today() - delta
99 filter_from = filter_from.replace(hour=0, minute=0, second=0)
100
101 elif re.search(r'(\d+[wW]{1})', stamp_filter):
102 number = int(stamp_filter.lower().replace('w', '')) * 7
103 delta = timedelta(days=number)
104 filter_from = datetime.today() - delta
105 filter_from = filter_from.replace(hour=0, minute=0, second=0)
106
107 elif re.search(r'(\d+[mM]{1})', stamp_filter):
108 number = int(stamp_filter.lower().replace('m', ''))
109 past = date.today()
110 # start travelling in time, back to N months ago
111 for n in range(number):
112 past = past.replace(day=1) - timedelta(days=1)
113 # Now use the year/month from the past + the current day to set
114 # the proper date
115 filter_from = datetime(past.year, past.month, date.today().day)
116
117 elif re.search(r'(\d+[yY]{1})', stamp_filter):
118 number = int(stamp_filter.lower().replace('y', ''))
119 today = date.today()
120 filter_from = datetime(today.year - number, today.month, today.day)
121
122 else:
123 # maybe they are giving us a fixed date
124 filter_from = datetime.strptime(stamp_filter, DATE_FORMAT)
125 filter_from = filter_from.replace(hour=0, minute=0, second=0)
126 filter_to = filter_from + timedelta(days=1)
127
128 return filter_from, filter_to
129
130 def customers(self):
131 customers = []
132 for stamp in self.stamps:
133 if stamp['customer'] not in customers:
134 customers.append(stamp['customer'])
135 customers.remove(None)
136 return customers
137
138 def totals(self, filter_from=None, filter_to=None):
139 totals = {}
140 for stamp in self.stamps:
141 customer = stamp['customer']
142 # customer will be None for "start" stamps, having no end time
143 if customer:
144 start = datetime.strptime(stamp['start'], DATETIME_FORMAT)
145 end = datetime.strptime(stamp['end'], DATETIME_FORMAT)
146 if filter_from and start < filter_from:
147 # if there is a filter setting a starting date for the
148 # report and the current stamp is from an earlier date, do
149 # not add it to the totals
150 continue
151 if filter_to and start > filter_to:
152 # similar for the end date
153 continue
154 if customer not in totals:
155 totals[customer] = 0
156 totals[customer] += self.worktime(stamp['start'], stamp['end'])
157 return totals
158
159 def details(self, filter_customer=None, filter_from=None, filter_to=None):
160 details = OrderedDict()
161 totals = OrderedDict()
162 total_customer = OrderedDict()
163 for stamp in self.stamps:
164 customer = stamp['customer']
165 if customer:
166 if filter_customer and customer != filter_customer:
167 # we are getting the details for only one customer, if this
168 # stamp is not for that customer, simply move on and ignore
169 # it
170 continue
171 start = datetime.strptime(stamp['start'], DATETIME_FORMAT)
172 start_day = start.strftime('%Y-%m-%d')
173 end = datetime.strptime(stamp['end'], DATETIME_FORMAT)
174 if filter_from and start < filter_from:
175 # if there is a filter setting a starting date for the
176 # report and the current stamp is from an earlier date, do
177 # not add it to the totals
178 continue
179 if filter_to and start > filter_to:
180 # similar for the end date
181 continue
182 # avoid "start" stamps
183 if start_day not in details:
184 details[start_day] = []
185 if start_day not in totals:
186 totals[start_day] = 0
187 worktime = self.worktime(stamp['start'], stamp['end'])
188 details[start_day].append(
189 ' -> %(worktime)s %(customer)s %(action)s' % {
190 'worktime': str(timedelta(seconds=worktime)),
191 'customer': customer,
192 'action': stamp['action']
193 })
194 totals[start_day] += worktime
195 if start_day not in total_customer:
196 total_customer[start_day] = {}
197 if customer not in total_customer[start_day]:
198 total_customer[start_day][customer] = 0
199 total_customer[start_day][customer] += worktime
200 for day in totals:
201 totals[day] = str(timedelta(seconds=totals[day]))
202 return details, totals, total_customer
203
204 def show_stamps(self, customer=None, stamp_filter=None, verbose=False,
205 sum=False, graph=False):
206
207 filter_from, filter_to = self.validate_filter(stamp_filter)
208
209 # If the user asks for verbose information, show it before the
210 # totals (mimicing what the original stamp tool does)
211 if verbose:
212 details, totals, total_customer = self.details(customer,
213 filter_from,
214 filter_to)
215 for day in details:
216 print('------ %(day)s ------' % {'day': day})
217 for line in details[day]:
218 print(line)
219 print(' Total: %(total)s' % {'total': totals[day]})
220 print '-'*79
221
222 # now calculate the totals and show them
223 totals = self.totals(filter_from, filter_to)
224 if customer:
225 seconds=totals.get(customer, 0)
226 total = timedelta(seconds=totals.get(customer, 0))
227 print(' %(customer)s: %(total)s' % {'customer': customer,
228 'total': total})
229 else:
230 for c in totals:
231 seconds=totals[c]
232 total = timedelta(seconds=totals[c])
233 print(' %(customer)s: %(total)s' % {'customer': c,
234 'total': total})
235
236 if sum:
237 sum_tot = ''
238 if totals:
239 print('------ Totals ------' % {'day': day})
240 for day, tot in totals.iteritems():
241 print(' %(day)s: %(total)s' % {'day': day, 'total': tot})
242 sum_tot = "%(total)s %(new)s" % {
243 'total': sum_tot,
244 'new': total
245 }
246 totalSecs, sec = divmod(seconds, 60)
247 hr, min = divmod(totalSecs, 60)
248 totalDays, remaining = divmod(seconds, SECS_DAY)
249 remainingMin, remainingSec = divmod(remaining, (60))
250 remainingHr, remainingMin = divmod(remainingMin, (60))
251 print('----- %d:%02d:%02d -----' % (hr, min, sec))
252 print('--- %d days, remaining: %d:%02d (%d hours/day) ---' % (
253 totalDays, remainingHr, remainingMin, HOURS_DAY
254 ))
255
256 if graph:
257 DAYS = 15
258 list_days = []
259 list_tot = []
260 stackedbar_chart = pygal.StackedBar()
261 stackedbar_chart.title = 'Worked time per day (in hours)'
262
263 if customer:
264 for day, tot in totals.iteritems():
265 list_days.append(day)
266 (h, m, s) = tot.split(':')
267 tot_sec = int(h) * 3600 + int(m) * 60 + int(s)
268 tot_h = float(tot_sec / float(60) / float(60))
269 list_tot.append(tot_h)
270 stackedbar_chart.add(customer, list_tot)
271 stackedbar_chart.x_labels = map(str, list_days)
272 stackedbar_chart.render_to_file('graphs/chart-%s.svg' % customer )
273 else:
274 all_customers = self.customers()
275 total_per_customer = {}
276 details, totals, total_customer = self.details()
277 chars = 0
278 total_customer_reverse = total_customer.items()
279 total_customer_reverse.reverse()
280 for day, tot in total_customer_reverse:
281 if chars < DAYS:
282 list_days.append(day)
283 for cust in self.customers():
284 if cust not in tot:
285 tot[cust] = 0
286 for cus, time in tot.iteritems():
287 tot_h = float(time / float(60) / float(60))
288 if cus not in total_per_customer:
289 total_per_customer[cus] = []
290 total_per_customer[cus].append(tot_h)
291 chars = chars + 1
292 for ccus, ctime in total_per_customer.iteritems():
293 stackedbar_chart.add(ccus, ctime)
294 stackedbar_chart.x_labels = map(str, list_days[-DAYS:])
295 stackedbar_chart.render_to_file('graphs/chart-all.svg')
Note: See TracBrowser for help on using the repository browser.