Merge lp:~jonatan-cloutier/gtimelog/addTags into lp:~gtimelog-dev/gtimelog/trunk

Proposed by Jonatan Cloutier
Status: Needs review
Proposed branch: lp:~jonatan-cloutier/gtimelog/addTags
Merge into: lp:~gtimelog-dev/gtimelog/trunk
Diff against target: 282 lines (+105/-23)
2 files modified
src/gtimelog/gtimelog.ui (+12/-0)
src/gtimelog/main.py (+93/-23)
To merge this branch: bzr merge lp:~jonatan-cloutier/gtimelog/addTags
Reviewer Review Type Date Requested Status
GTimeLog developers Pending
Review via email: mp+153561@code.launchpad.net

Description of the change

Add a tagging feature

To post a comment you must log in.

Unmerged revisions

255. By Jonatan Cloutier

add tags feature

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/gtimelog/gtimelog.ui'
2--- src/gtimelog/gtimelog.ui 2012-08-23 05:20:13 +0000
3+++ src/gtimelog/gtimelog.ui 2013-03-15 14:20:28 +0000
4@@ -292,6 +292,18 @@
5 </object>
6 </child>
7 <child>
8+ <object class="GtkRadioMenuItem" id="tags">
9+ <property name="visible">True</property>
10+ <property name="can_focus">False</property>
11+ <property name="use_action_appearance">False</property>
12+ <property name="label" translatable="yes">T_ags</property>
13+ <property name="use_underline">True</property>
14+ <property name="group">chronological</property>
15+ <accelerator key="4" signal="activate" modifiers="GDK_MOD1_MASK"/>
16+ <signal name="activate" handler="on_tags_activate" swapped="no"/>
17+ </object>
18+ </child>
19+ <child>
20 <object class="GtkSeparatorMenuItem" id="separator2">
21 <property name="visible">True</property>
22 <property name="can_focus">False</property>
23
24=== modified file 'src/gtimelog/main.py'
25--- src/gtimelog/main.py 2013-02-07 08:33:21 +0000
26+++ src/gtimelog/main.py 2013-03-15 14:20:28 +0000
27@@ -350,7 +350,7 @@
28 duration = stop - start
29 return start, stop, duration, entry
30
31- def grouped_entries(self, skip_first=True):
32+ def grouped_entries(self, skip_first=True, trimTags=True):
33 """Return consolidated entries (grouped by entry title).
34
35 Returns two list: work entries and slacking entries. Slacking
36@@ -360,6 +360,8 @@
37 work = {}
38 slack = {}
39 for start, stop, duration, entry in self.all_entries():
40+ if trimTags:
41+ entry = entry.split(" '")[0]
42 if skip_first:
43 skip_first = False
44 continue
45@@ -411,6 +413,35 @@
46 None, datetime.timedelta(0)) + duration
47 return entries, totals
48
49+ def tagged_work_entries(self, skip_first=True):
50+ """Return consolidated work entries grouped by tag.
51+
52+ Tag is a string following an apostrophe (') at the end of an entry.
53+
54+ Return two dicts:
55+ - {<tag>: <entry list>}, where <tag> is a tag string
56+ and <entry list> is a sorted list that contains tuples (start,
57+ entry, duration); entry is stripped of its tag. An entry can
58+ be present in more than one tag.
59+ - {<tag>: <total duration>}, where <total duration> is the
60+ total duration of work in the <category>.
61+ """
62+
63+ work, slack = self.grouped_entries(skip_first=skip_first, trimTags=False)
64+ entries = {}
65+ totals = {}
66+ for start, entry, duration in work:
67+ if " '" in entry:
68+ splitedEntries = entry.split(" '")
69+ tags = splitedEntries[1:]
70+ clipped_entry = splitedEntries[0]
71+ for tag in tags:
72+ entry_list = entries.get(tag, [])
73+ entry_list.append((start, clipped_entry, duration))
74+ entries[tag] = entry_list
75+ totals[tag] = totals.get(tag, datetime.timedelta(0)) + duration
76+ return entries, totals
77+
78 def totals(self):
79 """Calculate total time of work and slacking entries.
80
81@@ -637,8 +668,31 @@
82 print >> output
83 print >> output, "By category:"
84 print >> output
85-
86- items = categories.items()
87+
88+ self._report_groups(output, categories)
89+
90+ def _report_tags(self, output, tags):
91+ """A helper method that lists time spent per tag.
92+
93+ Use this to add a section in a report looks similar to this:
94+
95+ Administration: 2 hours 1 min
96+ Coding: 18 hours 45 min
97+ Learning: 3 hours
98+
99+ tags is a dict of entries (<tag name>: <duration>).
100+ """
101+ print >> output
102+ print >> output, "By tag:"
103+ print >> output
104+
105+ self._report_groups(output, tags)
106+
107+ def _report_groups(self, output, groups):
108+ """A helper method that lists time spent per grouped entries,
109+ either categories or tags.
110+ """
111+ items = groups.items()
112 items.sort()
113 for cat, duration in items:
114 if not cat:
115@@ -647,9 +701,9 @@
116 print >> output, u"%-62s %s" % (
117 cat, format_duration_long(duration))
118
119- if None in categories:
120+ if None in groups:
121 print >> output, u"%-62s %s" % (
122- '(none)', format_duration_long(categories[None]))
123+ '(none)', format_duration_long(groups[None]))
124 print >> output
125
126 def _plain_report(self, output, email, who, subject, period_name,
127@@ -674,7 +728,6 @@
128 print >> output, " time"
129 work, slack = window.grouped_entries()
130 total_work, total_slacking = window.totals()
131- categories = {}
132 if work:
133 work = [(entry, duration) for start, entry, duration in work]
134 work.sort()
135@@ -682,14 +735,6 @@
136 if not duration:
137 continue # skip empty "arrival" entries
138
139- if ': ' in entry:
140- cat, task = entry.split(': ', 1)
141- categories[cat] = categories.get(
142- cat, datetime.timedelta(0)) + duration
143- else:
144- categories[None] = categories.get(
145- None, datetime.timedelta(0)) + duration
146-
147 entry = entry[:1].upper() + entry[1:]
148 if estimated_column:
149 print >> output, (u"%-46s %-14s %s" %
150@@ -701,9 +746,14 @@
151 print >> output, ("Total work done this %s: %s" %
152 (period_name, format_duration_long(total_work)))
153
154+ entries, categories = window.categorized_work_entries()
155 if categories:
156 self._report_categories(output, categories)
157
158+ entries, tags = window.tagged_work_entries()
159+ if tags:
160+ self._report_tags(output, tags)
161+
162 def weekly_report_categorized(self, output, email, who,
163 estimated_column=False):
164 """Format a weekly report with entries displayed under categories."""
165@@ -766,27 +816,24 @@
166 print >> output
167 work, slack = window.grouped_entries()
168 total_work, total_slacking = window.totals()
169- categories = {}
170 if work:
171 for start, entry, duration in work:
172 entry = entry[:1].upper() + entry[1:]
173 print >> output, u"%-62s %s" % (entry,
174 format_duration_long(duration))
175- if ': ' in entry:
176- cat, task = entry.split(': ', 1)
177- categories[cat] = categories.get(
178- cat, datetime.timedelta(0)) + duration
179- else:
180- categories[None] = categories.get(
181- None, datetime.timedelta(0)) + duration
182
183 print >> output
184 print >> output, ("Total work done: %s" %
185 format_duration_long(total_work))
186
187- if len(categories) > 0:
188+ entries, categories = window.categorized_work_entries()
189+ if categories:
190 self._report_categories(output, categories)
191
192+ entries, tags = window.tagged_work_entries()
193+ if tags:
194+ self._report_tags(output, tags)
195+
196 print >> output, 'Slacking:\n'
197
198 if slack:
199@@ -1053,6 +1100,7 @@
200 spreadsheet = 'xdg-open %s'
201 chronological = True
202 summary_view = False
203+ tags_view = False
204 show_tasks = True
205
206 enable_gtk_completion = True # False enables gvim-style completion
207@@ -1111,6 +1159,7 @@
208 config.set('gtimelog', 'spreadsheet', self.spreadsheet)
209 config.set('gtimelog', 'chronological', str(self.chronological))
210 config.set('gtimelog', 'summary_view', str(self.summary_view))
211+ config.set('gtimelog', 'tags_view', str(self.tags_view))
212 config.set('gtimelog', 'show_tasks', str(self.show_tasks))
213 config.set('gtimelog', 'gtk-completion',
214 str(self.enable_gtk_completion))
215@@ -1140,6 +1189,7 @@
216 self.spreadsheet = config.get('gtimelog', 'spreadsheet')
217 self.chronological = config.getboolean('gtimelog', 'chronological')
218 self.summary_view = config.getboolean('gtimelog', 'summary_view')
219+ self.tags_view = config.getboolean('gtimelog', 'tags_view')
220 self.show_tasks = config.getboolean('gtimelog', 'show_tasks')
221 self.enable_gtk_completion = config.getboolean('gtimelog',
222 'gtk-completion')
223@@ -1440,6 +1490,7 @@
224 self.chronological = (settings.chronological
225 and not settings.summary_view)
226 self.summary_view = settings.summary_view
227+ self.tags_view = settings.tags_view
228 self.show_tasks = settings.show_tasks
229 self.looking_at_date = None
230 self.entry_watchers = []
231@@ -1454,6 +1505,8 @@
232 chronological_menu_item.set_active(self.chronological)
233 summary_menu_item = builder.get_object('summary')
234 summary_menu_item.set_active(self.summary_view)
235+ tags_menu_item = builder.get_object('tags')
236+ tags_menu_item.set_active(self.tags_view)
237 show_task_pane_item = builder.get_object('show_task_pane')
238 show_task_pane_item.set_active(self.show_tasks)
239 # Now hook up signals.
240@@ -1568,6 +1621,13 @@
241 where = buffer.get_end_iter()
242 where.backward_cursor_position()
243 buffer.place_cursor(where)
244+ elif self.tags_view:
245+ entries, totals = window.tagged_work_entries()
246+ for category, duration in sorted(totals.items()):
247+ self.write_group(category or 'no category', duration)
248+ where = buffer.get_end_iter()
249+ where.backward_cursor_position()
250+ buffer.place_cursor(where)
251 else:
252 work, slack = window.grouped_entries()
253 for start, entry, duration in work + slack:
254@@ -1830,18 +1890,28 @@
255 """View -> Chronological"""
256 self.chronological = True
257 self.summary_view = False
258+ self.tags_view = False
259 self.populate_log()
260
261 def on_grouped_activate(self, widget):
262 """View -> Grouped"""
263 self.chronological = False
264 self.summary_view = False
265+ self.tags_view = False
266 self.populate_log()
267
268 def on_summary_activate(self, widget):
269 """View -> Summary"""
270 self.chronological = False
271 self.summary_view = True
272+ self.tags_view = False
273+ self.populate_log()
274+
275+ def on_tags_activate(self, widget):
276+ """View -> Tags"""
277+ self.chronological = False
278+ self.summary_view = False
279+ self.tags_view = True
280 self.populate_log()
281
282 def daily_window(self, day=None):

Subscribers

People subscribed via source and target branches