22
22
pygtk.require('2.0')
28
from hamster import dispatcher, storage, SHARED_DATA_DIR, stuff
29
from hamster.charting import Chart
30
from hamster.add_custom_fact import CustomFactController
31
from edit_activity import CustomFactController
32
import reports, widgets, graphics
33
from configuration import runtime
36
from itertools import groupby
37
from gettext import ngettext
32
39
import datetime as dt
39
self.glade = gtk.glade.XML(os.path.join(SHARED_DATA_DIR, "stats.glade"))
42
from hamster.i18n import C_
44
class ReportChooserDialog(gtk.Dialog):
46
# format, path, start_date, end_date
47
'report-chosen': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
48
(gobject.TYPE_STRING, gobject.TYPE_STRING,
49
gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
50
gobject.TYPE_PYOBJECT)),
51
'report-chooser-closed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
54
gtk.Dialog.__init__(self)
55
ui = stuff.load_ui_file("stats.ui")
56
self.dialog = ui.get_object('save_report_dialog')
58
self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
59
self.dialog.set_current_folder(os.path.expanduser("~"))
63
filter = gtk.FileFilter()
64
filter.set_name(_("HTML Report"))
65
filter.add_mime_type("text/html")
66
filter.add_pattern("*.html")
67
filter.add_pattern("*.htm")
68
self.filters[filter] = "html"
69
self.dialog.add_filter(filter)
71
filter = gtk.FileFilter()
72
filter.set_name(_("Tab-Separated Values (TSV)"))
73
filter.add_mime_type("text/plain")
74
filter.add_pattern("*.tsv")
75
filter.add_pattern("*.txt")
76
self.filters[filter] = "tsv"
77
self.dialog.add_filter(filter)
79
filter = gtk.FileFilter()
80
filter.set_name(_("XML"))
81
filter.add_mime_type("text/xml")
82
filter.add_pattern("*.xml")
83
self.filters[filter] = "xml"
84
self.dialog.add_filter(filter)
86
filter = gtk.FileFilter()
87
filter.set_name(_("iCal"))
88
filter.add_mime_type("text/calendar")
89
filter.add_pattern("*.ics")
90
self.filters[filter] = "ical"
91
self.dialog.add_filter(filter)
93
filter = gtk.FileFilter()
94
filter.set_name("All files")
95
filter.add_pattern("*")
96
self.dialog.add_filter(filter)
98
self.start_date = widgets.DateInput()
99
ui.get_object('from_date_box').add(self.start_date)
100
self.end_date = widgets.DateInput()
101
ui.get_object('to_date_box').add(self.end_date)
103
self.category_box = ui.get_object('category_box')
105
ui.get_object('save_button').connect("clicked", self.on_save_button_clicked)
106
ui.get_object('cancel_button').connect("clicked", self.on_cancel_button_clicked)
109
def show(self, start_date, end_date):
110
#set suggested name to something readable, replace backslashes with dots
111
#so the name is valid in linux
112
filename = "Time track %s - %s" % (start_date.strftime("%x").replace("/", "."),
113
end_date.strftime("%x").replace("/", "."))
114
self.dialog.set_current_name(filename)
116
self.start_date.set_date(start_date)
117
self.end_date.set_date(end_date)
119
#add unsorted category
120
button_all = gtk.CheckButton(C_("categories", "All").encode("utf-8"))
121
button_all.value = None
122
button_all.set_active(True)
124
def on_category_all_clicked(checkbox):
125
active = checkbox.get_active()
126
for checkbox in self.category_box.get_children():
127
checkbox.set_active(active)
129
button_all.connect("clicked", on_category_all_clicked)
130
self.category_box.attach(button_all, 0, 1, 0, 1)
132
categories = runtime.storage.get_category_list()
134
for category in categories:
140
button = gtk.CheckButton(category['name'].encode("utf-8"))
141
button.value = category['id']
142
button.set_active(True)
143
self.category_box.attach(button, col, col+1, row, row+1)
147
response = self.dialog.show_all()
150
self.dialog.present()
152
def on_save_button_clicked(self, widget):
153
path, format = None, None
156
if self.dialog.get_filter() in self.filters:
157
format = self.filters[self.dialog.get_filter()]
158
path = self.dialog.get_filename()
161
for button in self.category_box.get_children():
162
if button.get_active() and button.value:
163
categories.append(button.value)
165
# format, path, start_date, end_date
166
self.emit("report-chosen", format, path,
167
self.start_date.get_date().date(),
168
self.end_date.get_date().date(),
170
self.dialog.destroy()
173
def on_cancel_button_clicked(self, widget):
174
self.emit("report-chooser-closed")
175
self.dialog.destroy()
177
class TimeLine(graphics.Area):
183
graphics.Area.__init__(self)
184
self.start_date, self.end_date = None, None
185
self.draw_mode = None
186
self.max_hours = None
189
def draw(self, facts):
192
for date, date_facts in itertools.groupby(facts, lambda x: x["start_time"].date()):
193
date_facts = list(date_facts)
194
self.facts[date] = date_facts
195
self.max_hours = max(self.max_hours,
196
sum([fact["delta"].seconds / 60 / float(60) +
197
fact["delta"].days * 24 for fact in date_facts]))
199
start_date = facts[0]["start_time"].date()
200
end_date = facts[-1]["start_time"].date()
202
self.draw_mode = self.MODE_YEAR
203
self.start_date = start_date.replace(month=1, day=1)
204
self.end_date = end_date.replace(month=12, day=31)
208
#TODO - for now we have only the year mode
209
if start_date.year != end_date.year or start_date.month != end_date.month:
210
self.draw_mode = self.MODE_YEAR
211
self.start_date = start_date.replace(month=1, day=1)
212
self.end_date = end_date.replace(month=12, day=31)
213
elif start_date.strftime("%W") != end_date.strftime("%W"):
214
self.draw_mode = self.MODE_MONTH
215
self.start_date = start_date.replace(day=1)
216
self.end_date = end_date.replace(date =
217
calendar.monthrange(self.end_date.year,
218
self.end_date.month)[1])
219
elif start_date != end_date:
220
self.draw_mode = self.MODE_WEEK
222
self.draw_mode = self.MODE_DAY
231
if self.draw_mode != self.MODE_YEAR:
234
self.fill_area(0, 0, self.width, self.height, (0.975,0.975,0.975))
235
charting.set_color(self.context, (100,100,100))
237
self.set_value_range(x_min = 1, x_max = (self.end_date - self.start_date).days)
238
month_label_fits = True
239
for month in range(1, 13):
240
self.layout.set_text(calendar.month_abbr[month])
241
label_w, label_h = self.layout.get_pixel_size()
242
if label_w * 2 > self.x_factor * 30:
243
month_label_fits = False
247
ticker_date = self.start_date
251
for year in range(self.start_date.year, self.end_date.year + 1):
252
#due to how things lay over, we are putting labels on backwards, so that they don't overlap
254
self.context.set_line_width(1)
255
for month in range(1, 13):
256
for day in range(1, calendar.monthrange(year, month)[1] + 1):
257
ticker_pos = year_pos + ticker_date.timetuple().tm_yday
259
#if ticker_date.weekday() in [0, 6]:
260
# self.fill_area(ticker_pos * self.x_factor + 1, 20, self.x_factor, self.height - 20, (240, 240, 240))
261
# self.context.stroke()
264
if self.x_factor > 5:
265
self.move_to(ticker_pos, self.height - 20)
266
self.line_to(ticker_pos, self.height)
268
self.layout.set_text(ticker_date.strftime("%d"))
269
label_w, label_h = self.layout.get_pixel_size()
271
if label_w < self.x_factor / 1.2: #if label fits
272
self.context.move_to(self.get_pixel(ticker_pos) + 2,
274
self.context.show_layout(self.layout)
276
self.context.stroke()
279
facts_today = self.facts.get(ticker_date, [])
281
total_length = dt.timedelta()
282
for fact in facts_today:
283
total_length += fact["delta"]
284
total_length = total_length.seconds / 60 / 60.0 + total_length.days * 24
285
total_length = total_length / float(self.max_hours) * self.height - 16
286
self.fill_area(ticker_pos * self.x_factor,
287
self.height - total_length,
288
self.x_factor, total_length,
294
ticker_date += dt.timedelta(1)
300
month_pos = ticker_pos - calendar.monthrange(year, month)[1] + 1
302
self.move_to(month_pos, 0)
303
#self.line_to(month_pos, 20)
305
self.layout.set_text(dt.date(year, month, 1).strftime("%b"))
307
self.move_to(month_pos, 0)
308
self.context.show_layout(self.layout)
314
self.layout.set_text("%d" % year)
315
label_w, label_h = self.layout.get_pixel_size()
317
self.move_to(year_pos + 2 / self.x_factor, month_label_fits * label_h * 1.2)
319
self.context.show_layout(self.layout)
321
self.context.stroke()
323
year_pos = ticker_pos #save current state for next year
327
class StatsViewer(object):
328
def __init__(self, parent = None):
329
self.parent = parent# determine if app shut shut down on close
330
self._gui = stuff.load_ui_file("stats.ui")
40
331
self.window = self.get_widget('stats_window')
332
self.stat_facts = None
334
#id, caption, duration, date (invisible), description, category
335
self.fact_store = gtk.TreeStore(int, str, str, str, str, str, gobject.TYPE_PYOBJECT)
340
self.background = (0.975,0.975,0.975)
341
self.get_widget("graph_frame").modify_bg(gtk.STATE_NORMAL,
342
gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
343
self.get_widget("explore_frame").modify_bg(gtk.STATE_NORMAL,
344
gtk.gdk.Color(*[int(b*65536.0) for b in self.background]))
347
x_offset = 90 # let's nicely align all graphs
349
self.category_chart = charting.BarChart(background = self.background,
350
bar_base_color = (238,221,221),
351
legend_width = x_offset,
353
show_stack_labels = True
355
self.get_widget("totals_by_category").add(self.category_chart)
358
self.day_chart = charting.BarChart(background = self.background,
359
bar_base_color = (220, 220, 220),
364
self.get_widget("totals_by_day").add(self.day_chart)
367
self.activity_chart = charting.HorizontalBarChart(orient = "horizontal",
369
values_on_bars = True,
371
legend_width = x_offset,
372
value_format = "%.1f",
373
background = self.background,
374
bars_beveled = False,
376
self.get_widget("totals_by_activity").add(self.activity_chart);
379
self.view_date = dt.date.today()
382
self.start_date = self.view_date - \
383
dt.timedelta(self.view_date.weekday() + 1)
384
# look if we need to start on sunday or monday
385
self.start_date = self.start_date + \
386
dt.timedelta(stuff.locale_first_weekday())
388
self.end_date = self.start_date + dt.timedelta(6)
391
self.week_view = self.get_widget("week")
392
self.month_view = self.get_widget("month")
393
self.month_view.set_group(self.week_view)
394
self.day_view = self.get_widget("day")
395
self.day_view.set_group(self.week_view)
397
#initiate the form in the week view
398
self.week_view.set_active(True)
401
runtime.dispatcher.add_handler('activity_updated', self.after_activity_update)
402
runtime.dispatcher.add_handler('day_updated', self.after_fact_update)
404
selection = self.fact_tree.get_selection()
405
selection.connect('changed', self.on_fact_selection_changed,
407
self.popular_categories = [cat[0] for cat in runtime.storage.get_popular_categories()]
409
self._gui.connect_signals(self)
410
self.fact_tree.grab_focus()
412
self.timeline = TimeLine()
413
self.get_widget("explore_everything").add(self.timeline)
414
self.get_widget("explore_everything").show_all()
417
self.report_chooser = None
421
def init_stats(self):
422
self.stat_facts = runtime.storage.get_facts(dt.date(1970, 1, 1), dt.date.today())
424
by_year = stuff.totals(self.stat_facts,
425
lambda fact: fact["start_time"].year,
428
year_box = self.get_widget("year_box")
429
class YearButton(gtk.ToggleButton):
430
def __init__(self, label, year, on_clicked):
431
gtk.ToggleButton.__init__(self, label)
433
self.connect("clicked", on_clicked)
435
all_button = YearButton(C_("years", "All").encode("utf-8"),
437
self.on_year_changed)
438
year_box.pack_start(all_button)
439
self.bubbling = True # TODO figure out how to properly work with togglebuttons as radiobuttons
440
all_button.set_active(True)
441
self.bubbling = False # TODO figure out how to properly work with togglebuttons as radiobuttons
443
years = sorted(by_year.keys())
445
year_box.pack_start(YearButton(str(year), year, self.on_year_changed))
449
self.chart_category_totals = charting.HorizontalBarChart(value_format = "%.1f",
450
bars_beveled = False,
451
background = self.background,
454
self.get_widget("explore_category_totals").add(self.chart_category_totals)
457
self.chart_weekday_totals = charting.HorizontalBarChart(value_format = "%.1f",
458
bars_beveled = False,
459
background = self.background,
462
self.get_widget("explore_weekday_totals").add(self.chart_weekday_totals)
464
self.chart_weekday_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
466
background = self.background,
469
self.get_widget("explore_weekday_starts_ends").add(self.chart_weekday_starts_ends)
471
self.chart_category_starts_ends = charting.HorizontalDayChart(bars_beveled = False,
473
background = self.background,
476
self.get_widget("explore_category_starts_ends").add(self.chart_category_starts_ends)
479
#ah, just want summary look just like all the other text on the page
480
class CairoText(graphics.Area):
481
def __init__(self, background = None, fontsize = 10):
482
graphics.Area.__init__(self)
483
self.background = background
485
self.fontsize = fontsize
487
def set_text(self, text):
493
self.fill_area(0, 0, self.width, self.height, self.background)
495
default_font = pango.FontDescription(gtk.Style().font_desc.to_string())
496
default_font.set_size(self.fontsize * pango.SCALE)
497
self.layout.set_font_description(default_font)
499
#self.context.set_source_rgb(0,0,0)
500
self.layout.set_markup(self.text)
502
self.layout.set_width((self.width) * pango.SCALE)
503
self.context.move_to(0,0)
504
charting.set_color(self.context, charting.dark[8])
506
self.context.show_layout(self.layout)
508
self.explore_summary = CairoText(self.background)
509
self.get_widget("explore_summary").add(self.explore_summary)
510
self.get_widget("explore_summary").show_all()
512
def stats(self, year = None):
513
facts = self.stat_facts
515
facts = filter(lambda fact: fact["start_time"].year == year,
518
if not facts or (facts[-1]["start_time"] - facts[0]["start_time"]) < dt.timedelta(days=6):
519
self.get_widget("statistics_box").hide()
520
self.get_widget("explore_controls").hide()
521
label = self.get_widget("not_enough_records_label")
524
label.set_text(_("""There is no data to generate statistics yet.
525
A week of usage would be nice!"""))
527
label.set_text(_("Still collecting data — check back after a week has passed!"))
532
self.get_widget("statistics_box").show()
533
self.get_widget("explore_controls").show()
534
self.get_widget("not_enough_records_label").hide()
536
# All dates in the scope
537
self.timeline.draw(facts)
541
categories = stuff.totals(facts,
542
lambda fact: fact["category"],
543
lambda fact: fact['delta'].seconds / 60 / 60.0)
544
category_keys = sorted(categories.keys())
545
categories = [categories[key] for key in category_keys]
546
self.chart_category_totals.plot(category_keys, categories)
549
weekdays = stuff.totals(facts,
550
lambda fact: (fact["start_time"].weekday(),
551
fact["start_time"].strftime("%a")),
552
lambda fact: fact['delta'].seconds / 60 / 60.0)
554
weekday_keys = sorted(weekdays.keys(), key = lambda x: x[0]) #sort
555
weekdays = [weekdays[key] for key in weekday_keys] #get values in the order
556
weekday_keys = [key[1] for key in weekday_keys] #now remove the weekday and keep just the abbreviated one
557
self.chart_weekday_totals.plot(weekday_keys, weekdays)
560
split_minutes = 5 * 60 + 30 #the mystical hamster midnight
562
# starts and ends by weekday
564
for date, date_facts in groupby(facts, lambda fact: fact["start_time"].date()):
565
date_facts = list(date_facts)
566
weekday = (date_facts[0]["start_time"].weekday(),
567
date_facts[0]["start_time"].strftime("%a"))
568
by_weekday.setdefault(weekday, [])
570
start_times, end_times = [], []
571
for fact in date_facts:
572
start_time = fact["start_time"].time()
573
start_time = start_time.hour * 60 + start_time.minute
575
end_time = fact["end_time"].time()
576
end_time = end_time.hour * 60 + end_time.minute
578
if start_time < split_minutes:
579
start_time += 24 * 60
580
if end_time < start_time:
583
start_times.append(start_time)
584
end_times.append(end_time)
585
if start_times and end_times:
586
by_weekday[weekday].append((min(start_times), max(end_times)))
589
for day in by_weekday:
590
by_weekday[day] = (sum([fact[0] for fact in by_weekday[day]]) / len(by_weekday[day]),
591
sum([fact[1] for fact in by_weekday[day]]) / len(by_weekday[day]))
593
min_weekday = min([by_weekday[day][0] for day in by_weekday])
594
max_weekday = max([by_weekday[day][1] for day in by_weekday])
597
weekday_keys = sorted(by_weekday.keys(), key = lambda x: x[0])
598
weekdays = [by_weekday[key] for key in weekday_keys]
599
weekday_keys = [key[1] for key in weekday_keys] # get rid of the weekday number as int
602
# starts and ends by category
604
for date, date_facts in groupby(facts, lambda fact: fact["start_time"].date()):
605
date_facts = sorted(list(date_facts), key = lambda x: x["category"])
607
for category, category_facts in groupby(date_facts, lambda x: x["category"]):
608
category_facts = list(category_facts)
609
by_category.setdefault(category, [])
611
start_times, end_times = [], []
612
for fact in category_facts:
613
start_time = fact["start_time"]
614
start_time = start_time.hour * 60 + start_time.minute
616
end_time = fact["end_time"].time()
617
end_time = end_time.hour * 60 + end_time.minute
619
if start_time < split_minutes:
620
start_time += 24 * 60
621
if end_time < start_time:
624
start_times.append(start_time)
625
end_times.append(end_time)
627
if start_times and end_times:
628
by_category[category].append((min(start_times), max(end_times)))
630
for cat in by_category:
631
by_category[cat] = (sum([fact[0] for fact in by_category[cat]]) / len(by_category[cat]),
632
sum([fact[1] for fact in by_category[cat]]) / len(by_category[cat]))
634
min_category = min([by_category[day][0] for day in by_category])
635
max_category = max([by_category[day][1] for day in by_category])
637
category_keys = sorted(by_category.keys(), key = lambda x: x[0])
638
categories = [by_category[key] for key in category_keys]
641
#get starting and ending hours for graph and turn them into exact hours that divide by 3
642
min_hour = min([min_weekday, min_category]) / 60 * 60
643
max_hour = max([max_weekday, max_category]) / 60 * 60
645
self.chart_weekday_starts_ends.plot_day(weekday_keys, weekdays, min_hour, max_hour)
646
self.chart_category_starts_ends.plot_day(category_keys, categories, min_hour, max_hour)
654
# date format for the first record if the year has not been selected
655
# Using python datetime formatting syntax. See:
656
# http://docs.python.org/library/time.html#time.strftime
657
first_date = facts[0]["start_time"].strftime(C_("first record", "%b %d, %Y"))
659
# date of first record when year has been selected
660
# Using python datetime formatting syntax. See:
661
# http://docs.python.org/library/time.html#time.strftime
662
first_date = facts[0]["start_time"].strftime(C_("first record", "%(b)s %(d)s"))
664
summary += _("First activity was recorded on %s.") % \
665
("<b>%s</b>" % first_date)
668
total_delta = dt.timedelta(days=0)
670
total_delta += fact["delta"]
672
if total_delta.days > 1:
673
human_years_str = ngettext("%(num)s year",
675
total_delta.days / 365) % {
676
'num': "<b>%.2f</b>" % (total_delta.days / 365.0)}
677
working_years_str = ngettext("%(num)s year",
679
total_delta.days * 3 / 365) % {
680
'num': "<b>%.2f</b>" % (total_delta.days * 3 / 365.0) }
681
#FIXME: difficult string to properly pluralize
682
summary += " " + _("""Time tracked so far is %(human_days)s human days \
683
(%(human_years)s) or %(working_days)s working days (%(working_years)s).""") % {
684
"human_days": ("<b>%d</b>" % total_delta.days),
685
"human_years": human_years_str,
686
"working_days": ("<b>%d</b>" % (total_delta.days * 3)), # 8 should be pretty much an average working day
687
"working_years": working_years_str }
693
if not max_fact or fact["delta"] > max_fact["delta"]:
696
longest_date = max_fact["start_time"].strftime(
697
# How the date of the longest activity should be displayed in statistics
698
# Using python datetime formatting syntax. See:
699
# http://docs.python.org/library/time.html#time.strftime
700
C_("date of the longest activity", "%b %d, %Y"))
702
num_hours = max_fact["delta"].seconds / 60 / 60.0 + max_fact["delta"].days * 24
703
hours = "<b>%.1f</b>" % (num_hours)
705
summary += "\n" + ngettext("Longest continuous work happened on \
706
%(date)s and was %(hours)s hour.",
707
"Longest continuous work happened on \
708
%(date)s and was %(hours)s hours.",
709
int(num_hours)) % {"date": longest_date,
712
# total records (in selected scope)
713
summary += " " + ngettext("There is %s record.",
714
"There are %s records.",
715
len(facts)) % ("<b>%d</b>" % len(facts))
718
early_start, early_end = dt.time(5,0), dt.time(9,0)
719
late_start, late_end = dt.time(20,0), dt.time(5,0)
722
fact_count = len(facts)
723
def percent(condition):
724
matches = [fact for fact in facts if condition(fact)]
725
return round(len(matches) / float(fact_count) * 100)
728
early_percent = percent(lambda fact: early_start < fact["start_time"].time() < early_end)
729
late_percent = percent(lambda fact: fact["start_time"].time() > late_start or fact["start_time"].time() < late_end)
730
short_percent = percent(lambda fact: fact["delta"] <= dt.timedelta(seconds = 60 * 15))
733
summary += "\n\n" + _("Hamster would like to observe you some more!")
734
elif early_percent >= 20:
735
summary += "\n\n" + _("With %s percent of all facts starting before \
736
9am you seem to be an early bird." % ("<b>%d</b>" % early_percent))
737
elif late_percent >= 20:
738
summary += "\n\n" + _("With %s percent of all facts starting after \
739
11pm you seem to be a night owl." % ("<b>%d</b>" % late_percent))
740
elif short_percent >= 20:
741
summary += "\n\n" + _("With %s percent of all tasks being shorter \
742
than 15 minutes you seem to be a busy bee." % ("<b>%d</b>" % short_percent))
744
self.explore_summary.set_text(summary)
746
def setup_tree(self):
747
def parent_painter(column, cell, model, iter):
748
cell_text = model.get_value(iter, 1)
749
if model.iter_parent(iter) is None:
750
if model.get_path(iter) == (0,):
751
text = '<span weight="heavy">%s</span>' % cell_text
753
text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
755
cell.set_property('markup', text)
758
activity_name = stuff.escape_pango(cell_text)
759
description = stuff.escape_pango(model.get_value(iter, 4))
760
category = stuff.escape_pango(model.get_value(iter, 5))
762
markup = stuff.format_activity(activity_name,
765
pad_description = True)
766
cell.set_property('markup', markup)
768
def duration_painter(column, cell, model, iter):
769
cell.set_property('xalign', 1)
770
cell.set_property('yalign', 0)
773
text = model.get_value(iter, 2)
774
if model.iter_parent(iter) is None:
775
if model.get_path(iter) == (0,):
776
text = '<span weight="heavy">%s</span>' % text
778
text = '<span weight="heavy" rise="-20000">%s</span>' % text
779
cell.set_property('markup', text)
42
782
self.fact_tree = self.get_widget("facts")
43
783
self.fact_tree.set_headers_visible(False)
44
784
self.fact_tree.set_tooltip_column(1)
45
785
self.fact_tree.set_property("show-expanders", False)
47
nameColumn = gtk.TreeViewColumn(_("Name"))
788
nameColumn = gtk.TreeViewColumn()
48
789
nameColumn.set_expand(True)
49
790
nameCell = gtk.CellRendererText()
50
791
nameCell.set_property("ellipsize", pango.ELLIPSIZE_END)
51
792
nameColumn.pack_start(nameCell, True)
52
nameColumn.set_cell_data_func(nameCell, self.parent_painter)
793
nameColumn.set_cell_data_func(nameCell, parent_painter)
53
794
self.fact_tree.append_column(nameColumn)
55
timeColumn = gtk.TreeViewColumn(_("Duration"))
797
timeColumn = gtk.TreeViewColumn()
56
798
timeCell = gtk.CellRendererText()
57
799
timeColumn.pack_end(timeCell, True)
58
timeColumn.set_cell_data_func(timeCell, self.duration_painter)
800
timeColumn.set_cell_data_func(timeCell, duration_painter)
59
805
self.fact_tree.append_column(timeColumn)
61
self.fact_store = gtk.TreeStore(int, str, str, str, str) #id, caption, duration, date (invisible), description
62
807
self.fact_tree.set_model(self.fact_store)
64
x_offset = 80 # let's nicely align all graphs
66
self.day_chart = Chart(max_bar_width = 40,
67
collapse_whitespace = True,
68
legend_width = x_offset)
69
eventBox = gtk.EventBox()
70
place = self.get_widget("totals_by_day")
71
eventBox.add(self.day_chart);
74
self.category_chart = Chart(orient = "horizontal",
77
values_on_bars = True,
79
legend_width = x_offset)
80
eventBox = gtk.EventBox()
81
place = self.get_widget("totals_by_category")
82
eventBox.add(self.category_chart);
85
self.activity_chart = Chart(orient = "horizontal",
88
values_on_bars = True,
90
legend_width = x_offset)
91
eventBox = gtk.EventBox()
92
place = self.get_widget("totals_by_activity")
93
eventBox.add(self.activity_chart);
96
self.view_date = dt.date.today()
98
self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1) #set to monday
99
# look if we need to start on sunday or monday
100
self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
102
self.end_date = self.start_date + dt.timedelta(6)
105
self.day_view = self.get_widget("day")
106
self.week_view = self.get_widget("week")
107
self.month_view = self.get_widget("month")
109
self.week_view.set_group(self.day_view)
110
self.month_view.set_group(self.day_view)
112
#initiate the form in the week view
113
self.week_view.set_active(True)
116
dispatcher.add_handler('activity_updated', self.after_activity_update)
117
dispatcher.add_handler('day_updated', self.after_fact_update)
119
selection = self.fact_tree.get_selection()
120
selection.connect('changed', self.on_fact_selection_changed, self.fact_store)
122
self.glade.signal_autoconnect(self)
123
self.fact_tree.grab_focus()
126
def locale_first_weekday(self):
127
"""figure if week starts on monday or sunday"""
129
first_weekday = 6 #by default settle on monday
132
process = os.popen("locale first_weekday week-1stday")
133
week_offset, week_start = process.read().split('\n')[:2]
135
week_start = dt.date(*time.strptime(week_start, "%Y%m%d")[:3])
136
week_offset = dt.timedelta(int(week_offset) - 1)
137
beginning = week_start + week_offset
138
first_weekday = int(beginning.strftime("%w"))
140
print "WARNING - Failed to get first weekday from locale"
145
def parent_painter(self, column, cell, model, iter):
146
cell_text = model.get_value(iter, 1)
147
if model.iter_parent(iter) == None:
148
if model.get_path(iter) == (0,):
149
text = '<span weight="heavy">%s</span>' % cell_text
151
text = '<span weight="heavy" rise="-20000">%s</span>' % cell_text
153
cell.set_property('markup', text)
156
activity_name = stuff.escape_pango(cell_text)
157
description = stuff.escape_pango(model.get_value(iter, 4))
159
text = " %s" % activity_name
161
text+= """\n <span style="italic" size="small">%s</span>""" % (description)
163
cell.set_property('markup', text)
165
def duration_painter(self, column, cell, model, iter):
166
text = model.get_value(iter, 2)
167
if model.iter_parent(iter) == None:
168
if model.get_path(iter) == (0,):
169
text = '<span weight="heavy">%s</span>' % text
171
text = '<span weight="heavy" rise="-20000">%s</span>' % text
172
cell.set_property('markup', text)
175
self.fact_store.clear()
182
week = {"days": [], "totals": []}
184
facts = storage.get_facts(self.start_date, self.end_date)
809
def on_graph_frame_size_allocate(self, widget, new_size):
810
w = min(new_size.width / 4, 200)
812
self.activity_chart.legend_width = w
813
self.category_chart.legend_width = w
814
self.get_widget("totals_by_category").set_size_request(w + 40, -1)
816
def fill_tree(self, facts):
818
for day, facts in groupby(facts, lambda fact: fact["date"]):
819
day_dict[day] = sorted(list(facts),
820
key=lambda fact: fact["start_time"])
186
822
for i in range((self.end_date - self.start_date).days + 1):
187
823
current_date = self.start_date + dt.timedelta(i)
188
# date format in overview window fact listing
189
# prefix is "o_",letter after prefix is regular python format. you can use all of them
190
fact_date = _("%(o_A)s, %(o_b)s %(o_d)s") % stuff.dateDict(current_date, "o_")
192
day_row = self.fact_store.append(None, [-1,
195
current_date.strftime('%Y-%m-%d'),
197
by_day[self.start_date + dt.timedelta(i)] = {"duration": 0, "row_pointer": day_row}
200
start_date = fact["start_time"].date()
203
if fact["end_time"]: # not set if just started
204
delta = fact["end_time"] - fact["start_time"]
205
duration = 24 * delta.days + delta.seconds / 60
206
elif fact["start_time"].date() == dt.date.today():
207
delta = dt.datetime.now() - fact["start_time"]
208
duration = 24 * delta.days + delta.seconds / 60
210
self.fact_store.append(by_day[start_date]["row_pointer"],
212
fact["start_time"].strftime('%H:%M') + " " +
214
stuff.format_duration(duration),
215
fact["start_time"].strftime('%Y-%m-%d'),
219
if fact["name"] not in by_activity: by_activity[fact["name"]] = 0
220
if fact["category"] not in by_category: by_category[fact["category"]] = 0
223
by_day[start_date]["duration"] += duration
224
by_activity[fact["name"]] += duration
225
by_category[fact["category"]] += duration
228
if self.week_view.get_active():
232
date_sort = lambda a, b: (b[4] < a[4]) - (a[4] < b[4])
233
totals["by_day"] = []
236
self.fact_store.set_value(by_day[day]["row_pointer"], 2,
237
stuff.format_duration(by_day[day]["duration"]))
238
if (self.end_date - self.start_date).days < 20:
239
strday = stuff.locale_to_utf8(day.strftime('%a'))
240
totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, None, day])
242
# date format in month chart in overview window (click on "month" to see it)
243
# prefix is "m_", letter after prefix is regular python format. you can use all of them
244
strday = _("%(m_b)s %(m_d)s") % stuff.dateDict(day, "m_")
247
if day.weekday() in [5, 6]:
250
totals["by_day"].append([strday, by_day[day]["duration"] / 60.0, None, background, day])
251
totals["by_day"].sort(date_sort)
254
duration_sort = lambda a, b: (a[1] < b[1]) - (b[1] < a[1])
255
totals["by_activity"] = []
256
for activity in by_activity:
257
totals["by_activity"].append([activity, by_activity[activity] / 60.0])
258
totals["by_activity"].sort(duration_sort)
260
#now we will limit bars to 6 and sum everything else into others
261
if len(totals["by_activity"]) > 12:
264
for i in range(11, len(totals["by_activity"]) - 1):
265
other_total += totals["by_activity"][i][1]
267
totals["by_activity"] = totals["by_activity"][:11]
268
totals["by_activity"].append([_("Other"), other_total, 1])
269
totals["by_activity"].sort(duration_sort) #sort again, since maybe others summed is bigger
271
totals["by_category"] = []
272
for category in by_category:
273
totals["by_category"].append([category, by_category[category] / 60.0])
274
totals["by_category"].sort(duration_sort)
825
# Date format for the label in overview window fact listing
826
# Using python datetime formatting syntax. See:
827
# http://docs.python.org/library/time.html#time.strftime
828
fact_date = current_date.strftime(C_("overview list", "%A, %b %d"))
830
day_total = dt.timedelta()
831
for fact in day_dict.get(current_date, []):
832
day_total += fact["delta"]
834
day_row = self.fact_store.append(None,
837
stuff.format_duration(day_total),
838
current_date.strftime('%Y-%m-%d'),
843
for fact in day_dict.get(current_date, []):
844
self.fact_store.append(day_row,
846
fact["start_time"].strftime('%H:%M') + " " +
848
stuff.format_duration(fact["delta"]),
849
fact["start_time"].strftime('%Y-%m-%d'),
277
855
self.fact_tree.expand_all()
279
self.get_widget("report_button").set_sensitive(len(facts) > 0)
282
week["totals"] = totals
858
def do_charts(self, facts):
859
all_categories = self.popular_categories
862
#the single "totals" (by category) bar
863
category_sums = stuff.totals(facts, lambda fact: fact["category"],
864
lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
865
category_totals = [category_sums.get(cat, 0)
866
for cat in all_categories]
867
category_keys = ["%s %.1f" % (cat, category_sums.get(cat, 0.0))
868
for cat in all_categories]
869
self.category_chart.plot([_("Total")],
871
stack_keys = category_keys)
873
# day / category chart
874
all_days = [self.start_date + dt.timedelta(i)
875
for i in range((self.end_date - self.start_date).days + 1)]
877
by_date_cat = stuff.totals(facts,
878
lambda fact: (fact["date"],
880
lambda fact: stuff.duration_minutes(fact["delta"]) / 60.0)
881
res = [[by_date_cat.get((day, cat), 0)
882
for cat in all_categories] for day in all_days]
884
#show days or dates depending on scale
885
if (self.end_date - self.start_date).days < 20:
886
day_keys = [day.strftime("%a") for day in all_days]
888
# date format used in the overview graph when month view is selected
889
# Using python datetime formatting syntax. See:
890
# http://docs.python.org/library/time.html#time.strftime
891
day_keys = [day.strftime(C_("overview graph", "%b %d"))
894
self.day_chart.plot(day_keys, res, stack_keys = all_categories)
897
#totals by activity, disguised under a stacked bar chart to get category colors
898
activity_sums = stuff.totals(facts,
899
lambda fact: (fact["name"],
901
lambda fact: stuff.duration_minutes(fact["delta"]))
902
by_duration = sorted(activity_sums.items(),
903
key = lambda x: x[1],
905
by_duration_keys = [entry[0][0] for entry in by_duration]
907
category_sums = [[entry[1] / 60.0 * (entry[0][1] == cat)
908
for cat in all_categories] for entry in by_duration]
909
self.activity_chart.plot(by_duration_keys,
911
stack_keys = all_categories)
915
if self.day_view.get_active():
916
# date format for overview label when only single day is visible
917
# Using python datetime formatting syntax. See:
918
# http://docs.python.org/library/time.html#time.strftime
919
start_date_str = self.view_date.strftime(C_("single day overview",
921
# Overview label if looking on single day
922
overview_label = _(u"Overview for %(date)s") % \
923
({"date": start_date_str})
925
dates_dict = stuff.dateDict(self.start_date, "start_")
926
dates_dict.update(stuff.dateDict(self.end_date, "end_"))
288
dates_dict = stuff.dateDict(self.start_date, "start_")
289
dates_dict.update(stuff.dateDict(self.end_date, "end_"))
292
if self.start_date.year != self.end_date.year:
293
# overview label if start and end years don't match
294
# letter after prefixes (start_, end_) is the one of
295
# standard python date formatting ones- you can use all of them
296
overview_label = _(u"Overview for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
297
elif self.start_date.month != self.end_date.month:
298
#overview label if start and end month do not match
299
# letter after prefixes (start_, end_) is the one of
300
# standard python date formatting ones- you can use all of them
301
overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
303
#overview label for interval in same month
304
# letter after prefixes (start_, end_) is the one of
305
# standard python date formatting ones- you can use all of them
306
overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict
308
if self.day_view.get_active():
309
# overview label for single day
310
# letter after prefixes (start_, end_) is the one of
311
# standard python date formatting ones- you can use all of them
312
overview_label = _("Overview for %(start_B)s %(start_d)s, %(start_Y)s") % dates_dict
313
dayview_caption = _("Day")
314
elif self.week_view.get_active():
928
if self.start_date.year != self.end_date.year:
929
# overview label if start and end years don't match
930
# letter after prefixes (start_, end_) is the one of
931
# standard python date formatting ones- you can use all of them
932
# see http://docs.python.org/library/time.html#time.strftime
933
overview_label = _(u"Overview for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
934
elif self.start_date.month != self.end_date.month:
935
# overview label if start and end month do not match
936
# letter after prefixes (start_, end_) is the one of
937
# standard python date formatting ones- you can use all of them
938
# see http://docs.python.org/library/time.html#time.strftime
939
overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict
941
# overview label for interval in same month
942
# letter after prefixes (start_, end_) is the one of
943
# standard python date formatting ones- you can use all of them
944
# see http://docs.python.org/library/time.html#time.strftime
945
overview_label = _(u"Overview for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict
947
if self.week_view.get_active():
315
948
dayview_caption = _("Week")
949
elif self.month_view.get_active():
317
950
dayview_caption = _("Month")
320
label = self.get_widget("overview_label")
321
label.set_text(overview_label)
323
label2 = self.get_widget("dayview_caption")
324
label2.set_markup("<b>%s</b>" % (dayview_caption))
326
facts = self.get_facts()
328
self.day_chart.plot(facts["totals"]["by_day"])
329
self.category_chart.plot(facts["totals"]["by_category"])
330
self.activity_chart.plot(facts["totals"]["by_activity"])
952
dayview_caption = _("Day")
954
self.get_widget("overview_label").set_markup("<b>%s</b>" % overview_label)
955
self.get_widget("dayview_caption").set_markup("%s" % (dayview_caption))
961
if self.day_view.get_active():
962
facts = runtime.storage.get_facts(self.view_date)
964
facts = runtime.storage.get_facts(self.start_date, self.end_date)
967
self.get_widget("report_button").set_sensitive(len(facts) > 0)
968
self.fact_store.clear()
970
self.fill_tree(facts)
973
self.get_widget("graphs").hide()
974
self.get_widget("no_data_label").show()
978
self.get_widget("no_data_label").hide()
979
self.get_widget("graphs").show()
980
self.do_charts(facts)
336
984
def get_widget(self, name):
337
985
""" skip one variable (huh) """
338
return self.glade.get_widget(name)
986
return self._gui.get_object(name)
988
def on_pages_switch_page(self, notebook, page, pagenum):
991
for child in self.get_widget("year_box").get_children():
992
if child.get_active():
999
def on_year_changed(self, button):
1000
if self.bubbling: return
1002
for child in button.parent.get_children():
1003
if child != button and child.get_active():
1004
self.bubbling = True
1005
child.set_active(False)
1006
self.bubbling = False
1008
self.stats(button.year)
340
1010
def on_prev_clicked(self, button):
341
1011
if self.day_view.get_active():
342
self.start_date -= dt.timedelta(1)
343
self.end_date -= dt.timedelta(1)
345
elif self.week_view.get_active():
346
self.start_date -= dt.timedelta(7)
347
self.end_date -= dt.timedelta(7)
349
elif self.month_view.get_active():
350
self.end_date = self.start_date - dt.timedelta(1)
351
first_weekday, days_in_month = calendar.monthrange(self.end_date.year, self.end_date.month)
352
self.start_date = self.end_date - dt.timedelta(days_in_month - 1)
354
self.view_date = self.start_date
1012
self.view_date -= dt.timedelta(1)
1013
if self.view_date < self.start_date:
1014
self.start_date -= dt.timedelta(7)
1015
self.end_date -= dt.timedelta(7)
1017
if self.week_view.get_active():
1018
self.start_date -= dt.timedelta(7)
1019
self.end_date -= dt.timedelta(7)
1021
elif self.month_view.get_active():
1022
self.end_date = self.start_date - dt.timedelta(1)
1023
first_weekday, days_in_month = calendar.monthrange(self.end_date.year, self.end_date.month)
1024
self.start_date = self.end_date - dt.timedelta(days_in_month - 1)
1026
self.view_date = self.start_date
357
1030
def on_next_clicked(self, button):
358
1031
if self.day_view.get_active():
359
self.start_date += dt.timedelta(1)
360
self.end_date += dt.timedelta(1)
362
elif self.week_view.get_active():
363
self.start_date += dt.timedelta(7)
364
self.end_date += dt.timedelta(7)
366
elif self.month_view.get_active():
367
self.start_date = self.end_date + dt.timedelta(1)
368
first_weekday, days_in_month = calendar.monthrange(self.start_date.year, self.start_date.month)
369
self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
371
self.view_date = self.start_date
1032
self.view_date += dt.timedelta(1)
1033
if self.view_date > self.end_date:
1034
self.start_date += dt.timedelta(7)
1035
self.end_date += dt.timedelta(7)
1037
if self.week_view.get_active():
1038
self.start_date += dt.timedelta(7)
1039
self.end_date += dt.timedelta(7)
1040
elif self.month_view.get_active():
1041
self.start_date = self.end_date + dt.timedelta(1)
1042
first_weekday, days_in_month = calendar.monthrange(self.start_date.year, self.start_date.month)
1043
self.end_date = self.start_date + dt.timedelta(days_in_month - 1)
1045
self.view_date = self.start_date
374
1049
def on_home_clicked(self, button):
375
1050
self.view_date = dt.date.today()
376
if self.day_view.get_active():
377
self.start_date = self.view_date
378
self.end_date = self.view_date
380
elif self.week_view.get_active():
1051
if self.week_view.get_active():
381
1052
self.start_date = self.view_date - dt.timedelta(self.view_date.weekday() + 1)
382
self.start_date = self.start_date + dt.timedelta(self.locale_first_weekday())
1053
self.start_date = self.start_date + dt.timedelta(stuff.locale_first_weekday())
383
1054
self.end_date = self.start_date + dt.timedelta(6)
385
1056
elif self.month_view.get_active():