Merge lp:~justas.sadzevicius/gtimelog/recent_tasks into lp:~gtimelog-dev/gtimelog/trunk

Proposed by Marius Gedminas
Status: Work in progress
Proposed branch: lp:~justas.sadzevicius/gtimelog/recent_tasks
Merge into: lp:~gtimelog-dev/gtimelog/trunk
Diff against target: 242 lines (+189/-1)
1 file modified
src/gtimelog/main.py (+189/-1)
To merge this branch: bzr merge lp:~justas.sadzevicius/gtimelog/recent_tasks
Reviewer Review Type Date Requested Status
GTimeLog developers Pending
Review via email: mp+46582@code.launchpad.net

Description of the change

Cue 80s style commercial:

  Keep your time log activity list relevant and up to date!

  Act now and save (time)! *

  Only the best task lists brought to your doorstep by Justas'
Launchpad branch.

  * lp:~justas-pov/gtimelog/recent_tasks
  * unit tests sold separately

Cheers,
Justas

P.S.: my gtimelogrc config:

task_list_url =
recent_task_days = 14
max_recent_category_tasks = 6
max_total_recent_tasks = 14
max_top_duration = 7
max_top_recent = 7
max_top_days_worked = 6

(empty task_list_url and specified recent_task_days enable the task list)

P.P.S.:

"recent_task_days" - how many (calendar) days to aggregate

"max_recent_category_tasks" - max task entries per category

"max_total_recent_tasks" - max total of tasks in the list

Tasks are filtered by three criteria, then merged to a single list.

You can limit max amount of tasks for each filter:
"max_top_duration" - tasks you spent most time working on
"max_top_recent" - tasks you worked on most recently
"max_top_days_worked" - tasks you spend most days on

To post a comment you must log in.
Revision history for this message
Marius Gedminas (mgedmin) wrote :

I want something like this.

I'm not happy that it creates a new file, mirroring the information already loaded into memory from timelog.txt. Is that really necessary?

I'd like to make the sidebar capable of displaying multiple pieces of information. Tasks and Recent Tasks could be two alternatives, selectable from a dropdown that would replace the title label.

That's a lot of new settings. And I get the impression that a lot of them are 0 and therefore disable this feature. Can we get some reasonable defaults that make it work out of the box?

Revision history for this message
Justas Sadzevičius (justas.sadzevicius) wrote :

> I want something like this.
>
> I'm not happy that it creates a new file, mirroring the information already
> loaded into memory from timelog.txt. Is that really necessary?

No, it was a quick hack.

> I'd like to make the sidebar capable of displaying multiple pieces of
> information. Tasks and Recent Tasks could be two alternatives, selectable
> from a dropdown that would replace the title label.

Oh, that would be nice!

> That's a lot of new settings. And I get the impression that a lot of them are
> 0 and therefore disable this feature. Can we get some reasonable defaults
> that make it work out of the box?

+1 for reasonable defaults. And I agree on too many settings.

At this point I just wanted to fiddle with them all to keep the actual list as short as possible. When displaying, say, only 15 tasks it's quite difficult to decide what makes the cut and what does not.

For now, I'd like to do some "field testing" and see what happens.

Unmerged revisions

178. By Justas Sadzevičius

Reload task list if gtimelogrc changed.

177. By Justas Sadzevičius

More filtering options.

176. By Justas Sadzevičius

Fast-and-dirty "recent" task list to pick from most recent, most worked on and most often worked on tasks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/gtimelog/main.py'
2--- src/gtimelog/main.py 2011-01-13 22:15:15 +0000
3+++ src/gtimelog/main.py 2011-01-18 11:06:11 +0000
4@@ -12,6 +12,7 @@
5 import urllib
6 import datetime
7 import tempfile
8+import itertools
9 import ConfigParser
10
11 import pygtk
12@@ -694,7 +695,7 @@
13 """
14 mtime = self.get_mtime()
15 if mtime != self.last_mtime:
16- self.load()
17+ self.reload()
18 return True
19 else:
20 return False
21@@ -794,6 +795,13 @@
22 task_list_url = ''
23 edit_task_list_cmd = ''
24
25+ recent_task_days = 0
26+ max_recent_category_tasks = 15
27+ max_total_recent_tasks = 50
28+ max_top_recent_tasks = 0
29+ max_top_duration_tasks = 0
30+ max_top_days_worked_tasks = 0
31+
32 show_office_hours = True
33 show_tray_icon = True
34 prefer_app_indicator = True
35@@ -816,6 +824,17 @@
36 self.virtual_midnight.strftime('%H:%M'))
37 config.set('gtimelog', 'task_list_url', self.task_list_url)
38 config.set('gtimelog', 'edit_task_list_cmd', self.edit_task_list_cmd)
39+ config.set('gtimelog', 'recent_task_days', self.recent_task_days)
40+ config.set('gtimelog', 'max_recent_category_tasks',
41+ self.max_recent_category_tasks)
42+ config.set('gtimelog', 'max_total_recent_tasks',
43+ self.max_total_recent_tasks)
44+ config.set('gtimelog', 'max_top_recent_tasks',
45+ self.max_top_recent_tasks)
46+ config.set('gtimelog', 'max_top_duration_tasks',
47+ self.max_top_duration_tasks)
48+ config.set('gtimelog', 'max_top_days_worked_tasks',
49+ self.max_top_days_worked_tasks)
50 config.set('gtimelog', 'show_office_hours',
51 str(self.show_office_hours))
52 config.set('gtimelog', 'show_tray_icon', str(self.show_tray_icon))
53@@ -840,6 +859,18 @@
54 'virtual_midnight'))
55 self.task_list_url = config.get('gtimelog', 'task_list_url')
56 self.edit_task_list_cmd = config.get('gtimelog', 'edit_task_list_cmd')
57+ self.recent_task_days = config.getint('gtimelog',
58+ 'recent_task_days')
59+ self.max_recent_category_tasks = config.getint('gtimelog',
60+ 'max_recent_category_tasks')
61+ self.max_total_recent_tasks = config.getint('gtimelog',
62+ 'max_total_recent_tasks')
63+ self.max_top_recent_tasks = config.getint('gtimelog',
64+ 'max_top_recent_tasks')
65+ self.max_top_duration_tasks = config.getint('gtimelog',
66+ 'max_top_duration_tasks')
67+ self.max_top_days_worked_tasks = config.getint('gtimelog',
68+ 'max_top_days_worked_tasks')
69 self.show_office_hours = config.getboolean('gtimelog',
70 'show_office_hours')
71 self.show_tray_icon = config.getboolean('gtimelog', 'show_tray_icon')
72@@ -858,6 +889,146 @@
73 f.close()
74
75
76+class RecentTaskList(TaskList):
77+ """Task list picked out from the time log.
78+
79+ Keeps a cached copy of the list in a local file, so you can use it offline.
80+ """
81+
82+ def __init__(self, cache_filename, timelog_filename,
83+ recent_days=0,
84+ max_category_tasks=0,
85+ max_total_tasks=0,
86+ max_top_recent=0,
87+ max_top_duration=0,
88+ max_top_days_worked=0,
89+ virtual_midnight=datetime.time(0, 0)):
90+ self.timelog_filename = timelog_filename
91+ self.max_category_tasks=max_category_tasks
92+ self.max_total_tasks=max_total_tasks
93+ self.max_top_recent=max_top_recent
94+ self.max_top_duration=max_top_duration
95+ self.max_top_days_worked=max_top_days_worked
96+ self.recent_days=recent_days
97+ self.virtual_midnight=virtual_midnight
98+ TaskList.__init__(self, cache_filename)
99+ self.first_time = True
100+
101+ def get_mtime(self):
102+ """Return mtime tuple of (self.filename, self.timelog_filename),
103+ or None if the one of them doesn't exist.
104+ """
105+ try:
106+ return max(os.stat(self.filename).st_mtime,
107+ os.stat(self.timelog_filename).st_mtime)
108+ except OSError:
109+ return None
110+
111+ def check_reload(self):
112+ if self.first_time:
113+ self.first_time = False
114+ if not os.path.exists(self.filename):
115+ self.generate()
116+ self.load()
117+ return True
118+ return TaskList.check_reload(self)
119+
120+ def top_recent(self, entries):
121+ result = []
122+ for start, stop, duration, entry in entries:
123+ if entry in result:
124+ result.remove(entry)
125+ result.insert(0, entry)
126+ return result
127+
128+ def top_duration(self, entries):
129+ entry_duration = {}
130+ for start, stop, duration, entry in entries:
131+ total_duration = entry_duration.get(entry, datetime.timedelta(0))
132+ total_duration += duration
133+ entry_duration[entry] = total_duration
134+
135+ sorted_entries = sorted(entry_duration.items(),
136+ key=lambda e: e[1],
137+ reverse=True)
138+ result = [e[0] for e in sorted_entries]
139+ return result
140+
141+ def top_days_worked(self, entries):
142+ entry_days = {}
143+ for start, stop, duration, entry in entries:
144+ day = virtual_day(start, self.virtual_midnight)
145+ entry_days.setdefault(entry, set()).add(day)
146+ sorted_entries = sorted(entry_days.items(),
147+ key=lambda e: len(e[1]),
148+ reverse=True)
149+ result = [e[0] for e in sorted_entries]
150+ return result
151+
152+ def generate(self):
153+ """Generate the task list from time log."""
154+ virtual_tomorrow = virtual_day(
155+ datetime.datetime.now() + datetime.timedelta(1),
156+ self.virtual_midnight)
157+
158+ dt_max = datetime.datetime.combine(virtual_tomorrow, self.virtual_midnight)
159+ dt_min = dt_max - datetime.timedelta(self.recent_days)
160+
161+ time_window = TimeWindow(
162+ self.timelog_filename, dt_min, dt_max,
163+ self.virtual_midnight)
164+
165+ # XXX: load to memory... ouch.
166+ all_entries = list(time_window.all_entries())
167+
168+ top_recent = self.top_recent(all_entries)
169+ if self.max_top_recent:
170+ top_recent = top_recent[:self.max_top_recent]
171+ top_duration = self.top_duration(all_entries)
172+ if self.max_top_duration:
173+ top_duration = top_duration[:self.max_top_duration]
174+ top_days_worked = self.top_days_worked(all_entries)
175+ if self.max_top_days_worked:
176+ top_days_worked = top_days_worked[:self.max_top_days_worked]
177+
178+ recent = []
179+ for entries in itertools.izip_longest(top_recent,
180+ top_duration,
181+ top_days_worked):
182+ for entry in filter(None, entries):
183+ if ': ' in entry:
184+ cat_task = tuple([s.strip() for s in entry.split(': ', 1)])
185+ else:
186+ cat_task = ('Other', entry.strip())
187+ if cat_task not in recent:
188+ recent.append(cat_task)
189+
190+ recent = recent[:self.max_total_tasks]
191+
192+ by_category = {}
193+ for category, task in recent:
194+ by_category.setdefault(category, []).append(task)
195+ for category in by_category:
196+ tasks = by_category[category]
197+ tasks = tasks[:self.max_category_tasks]
198+ by_category[category] = tasks
199+
200+ separator = '#..%s..#' % '...x...'.join([str(d) for d in range(1, 7)])
201+ f = open(self.filename, "w")
202+ for category in sorted(by_category):
203+ tasks = by_category[category]
204+ print >> f, separator
205+ print >> f
206+ for task in tasks:
207+ print >> f, '%s: %s' % (category, task)
208+ print >> f
209+ f.close()
210+
211+ def reload(self):
212+ self.generate()
213+ self.load()
214+
215+
216 class IconChooser(object):
217
218 @property
219@@ -1796,6 +1967,23 @@
220 if settings.task_list_url:
221 tasks = RemoteTaskList(settings.task_list_url,
222 os.path.join(configdir, 'remote-tasks.txt'))
223+ elif settings.recent_task_days:
224+ tasks = RecentTaskList(
225+ os.path.join(configdir, 'recent-tasks.txt'),
226+ os.path.join(configdir, 'timelog.txt'),
227+ recent_days=settings.recent_task_days,
228+ max_category_tasks=settings.max_recent_category_tasks,
229+ max_total_tasks=settings.max_total_recent_tasks,
230+ max_top_recent=settings.max_top_recent_tasks,
231+ max_top_duration=settings.max_top_duration_tasks,
232+ max_top_days_worked=settings.max_top_days_worked_tasks,
233+ virtual_midnight=settings.virtual_midnight)
234+ try:
235+ if (os.stat(tasks.filename).st_mtime <
236+ os.stat(settings_file).st_mtime):
237+ tasks.reload()
238+ except OSError:
239+ pass
240 else:
241 tasks = TaskList(os.path.join(configdir, 'tasks.txt'))
242 main_window = MainWindow(timelog, settings, tasks)

Subscribers

People subscribed via source and target branches