Merge lp:~jonatan-cloutier/gtimelog/addTags into lp:~gtimelog-dev/gtimelog/trunk
- addTags
- Merge into 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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
GTimeLog developers | Pending | ||
Review via email:
|
Commit message
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 | 292 | </object> | 292 | </object> |
6 | 293 | </child> | 293 | </child> |
7 | 294 | <child> | 294 | <child> |
8 | 295 | <object class="GtkRadioMenuItem" id="tags"> | ||
9 | 296 | <property name="visible">True</property> | ||
10 | 297 | <property name="can_focus">False</property> | ||
11 | 298 | <property name="use_action_appearance">False</property> | ||
12 | 299 | <property name="label" translatable="yes">T_ags</property> | ||
13 | 300 | <property name="use_underline">True</property> | ||
14 | 301 | <property name="group">chronological</property> | ||
15 | 302 | <accelerator key="4" signal="activate" modifiers="GDK_MOD1_MASK"/> | ||
16 | 303 | <signal name="activate" handler="on_tags_activate" swapped="no"/> | ||
17 | 304 | </object> | ||
18 | 305 | </child> | ||
19 | 306 | <child> | ||
20 | 295 | <object class="GtkSeparatorMenuItem" id="separator2"> | 307 | <object class="GtkSeparatorMenuItem" id="separator2"> |
21 | 296 | <property name="visible">True</property> | 308 | <property name="visible">True</property> |
22 | 297 | <property name="can_focus">False</property> | 309 | <property name="can_focus">False</property> |
23 | 298 | 310 | ||
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 | 350 | duration = stop - start | 350 | duration = stop - start |
29 | 351 | return start, stop, duration, entry | 351 | return start, stop, duration, entry |
30 | 352 | 352 | ||
32 | 353 | def grouped_entries(self, skip_first=True): | 353 | def grouped_entries(self, skip_first=True, trimTags=True): |
33 | 354 | """Return consolidated entries (grouped by entry title). | 354 | """Return consolidated entries (grouped by entry title). |
34 | 355 | 355 | ||
35 | 356 | Returns two list: work entries and slacking entries. Slacking | 356 | Returns two list: work entries and slacking entries. Slacking |
36 | @@ -360,6 +360,8 @@ | |||
37 | 360 | work = {} | 360 | work = {} |
38 | 361 | slack = {} | 361 | slack = {} |
39 | 362 | for start, stop, duration, entry in self.all_entries(): | 362 | for start, stop, duration, entry in self.all_entries(): |
40 | 363 | if trimTags: | ||
41 | 364 | entry = entry.split(" '")[0] | ||
42 | 363 | if skip_first: | 365 | if skip_first: |
43 | 364 | skip_first = False | 366 | skip_first = False |
44 | 365 | continue | 367 | continue |
45 | @@ -411,6 +413,35 @@ | |||
46 | 411 | None, datetime.timedelta(0)) + duration | 413 | None, datetime.timedelta(0)) + duration |
47 | 412 | return entries, totals | 414 | return entries, totals |
48 | 413 | 415 | ||
49 | 416 | def tagged_work_entries(self, skip_first=True): | ||
50 | 417 | """Return consolidated work entries grouped by tag. | ||
51 | 418 | |||
52 | 419 | Tag is a string following an apostrophe (') at the end of an entry. | ||
53 | 420 | |||
54 | 421 | Return two dicts: | ||
55 | 422 | - {<tag>: <entry list>}, where <tag> is a tag string | ||
56 | 423 | and <entry list> is a sorted list that contains tuples (start, | ||
57 | 424 | entry, duration); entry is stripped of its tag. An entry can | ||
58 | 425 | be present in more than one tag. | ||
59 | 426 | - {<tag>: <total duration>}, where <total duration> is the | ||
60 | 427 | total duration of work in the <category>. | ||
61 | 428 | """ | ||
62 | 429 | |||
63 | 430 | work, slack = self.grouped_entries(skip_first=skip_first, trimTags=False) | ||
64 | 431 | entries = {} | ||
65 | 432 | totals = {} | ||
66 | 433 | for start, entry, duration in work: | ||
67 | 434 | if " '" in entry: | ||
68 | 435 | splitedEntries = entry.split(" '") | ||
69 | 436 | tags = splitedEntries[1:] | ||
70 | 437 | clipped_entry = splitedEntries[0] | ||
71 | 438 | for tag in tags: | ||
72 | 439 | entry_list = entries.get(tag, []) | ||
73 | 440 | entry_list.append((start, clipped_entry, duration)) | ||
74 | 441 | entries[tag] = entry_list | ||
75 | 442 | totals[tag] = totals.get(tag, datetime.timedelta(0)) + duration | ||
76 | 443 | return entries, totals | ||
77 | 444 | |||
78 | 414 | def totals(self): | 445 | def totals(self): |
79 | 415 | """Calculate total time of work and slacking entries. | 446 | """Calculate total time of work and slacking entries. |
80 | 416 | 447 | ||
81 | @@ -637,8 +668,31 @@ | |||
82 | 637 | print >> output | 668 | print >> output |
83 | 638 | print >> output, "By category:" | 669 | print >> output, "By category:" |
84 | 639 | print >> output | 670 | print >> output |
87 | 640 | 671 | ||
88 | 641 | items = categories.items() | 672 | self._report_groups(output, categories) |
89 | 673 | |||
90 | 674 | def _report_tags(self, output, tags): | ||
91 | 675 | """A helper method that lists time spent per tag. | ||
92 | 676 | |||
93 | 677 | Use this to add a section in a report looks similar to this: | ||
94 | 678 | |||
95 | 679 | Administration: 2 hours 1 min | ||
96 | 680 | Coding: 18 hours 45 min | ||
97 | 681 | Learning: 3 hours | ||
98 | 682 | |||
99 | 683 | tags is a dict of entries (<tag name>: <duration>). | ||
100 | 684 | """ | ||
101 | 685 | print >> output | ||
102 | 686 | print >> output, "By tag:" | ||
103 | 687 | print >> output | ||
104 | 688 | |||
105 | 689 | self._report_groups(output, tags) | ||
106 | 690 | |||
107 | 691 | def _report_groups(self, output, groups): | ||
108 | 692 | """A helper method that lists time spent per grouped entries, | ||
109 | 693 | either categories or tags. | ||
110 | 694 | """ | ||
111 | 695 | items = groups.items() | ||
112 | 642 | items.sort() | 696 | items.sort() |
113 | 643 | for cat, duration in items: | 697 | for cat, duration in items: |
114 | 644 | if not cat: | 698 | if not cat: |
115 | @@ -647,9 +701,9 @@ | |||
116 | 647 | print >> output, u"%-62s %s" % ( | 701 | print >> output, u"%-62s %s" % ( |
117 | 648 | cat, format_duration_long(duration)) | 702 | cat, format_duration_long(duration)) |
118 | 649 | 703 | ||
120 | 650 | if None in categories: | 704 | if None in groups: |
121 | 651 | print >> output, u"%-62s %s" % ( | 705 | print >> output, u"%-62s %s" % ( |
123 | 652 | '(none)', format_duration_long(categories[None])) | 706 | '(none)', format_duration_long(groups[None])) |
124 | 653 | print >> output | 707 | print >> output |
125 | 654 | 708 | ||
126 | 655 | def _plain_report(self, output, email, who, subject, period_name, | 709 | def _plain_report(self, output, email, who, subject, period_name, |
127 | @@ -674,7 +728,6 @@ | |||
128 | 674 | print >> output, " time" | 728 | print >> output, " time" |
129 | 675 | work, slack = window.grouped_entries() | 729 | work, slack = window.grouped_entries() |
130 | 676 | total_work, total_slacking = window.totals() | 730 | total_work, total_slacking = window.totals() |
131 | 677 | categories = {} | ||
132 | 678 | if work: | 731 | if work: |
133 | 679 | work = [(entry, duration) for start, entry, duration in work] | 732 | work = [(entry, duration) for start, entry, duration in work] |
134 | 680 | work.sort() | 733 | work.sort() |
135 | @@ -682,14 +735,6 @@ | |||
136 | 682 | if not duration: | 735 | if not duration: |
137 | 683 | continue # skip empty "arrival" entries | 736 | continue # skip empty "arrival" entries |
138 | 684 | 737 | ||
139 | 685 | if ': ' in entry: | ||
140 | 686 | cat, task = entry.split(': ', 1) | ||
141 | 687 | categories[cat] = categories.get( | ||
142 | 688 | cat, datetime.timedelta(0)) + duration | ||
143 | 689 | else: | ||
144 | 690 | categories[None] = categories.get( | ||
145 | 691 | None, datetime.timedelta(0)) + duration | ||
146 | 692 | |||
147 | 693 | entry = entry[:1].upper() + entry[1:] | 738 | entry = entry[:1].upper() + entry[1:] |
148 | 694 | if estimated_column: | 739 | if estimated_column: |
149 | 695 | print >> output, (u"%-46s %-14s %s" % | 740 | print >> output, (u"%-46s %-14s %s" % |
150 | @@ -701,9 +746,14 @@ | |||
151 | 701 | print >> output, ("Total work done this %s: %s" % | 746 | print >> output, ("Total work done this %s: %s" % |
152 | 702 | (period_name, format_duration_long(total_work))) | 747 | (period_name, format_duration_long(total_work))) |
153 | 703 | 748 | ||
154 | 749 | entries, categories = window.categorized_work_entries() | ||
155 | 704 | if categories: | 750 | if categories: |
156 | 705 | self._report_categories(output, categories) | 751 | self._report_categories(output, categories) |
157 | 706 | 752 | ||
158 | 753 | entries, tags = window.tagged_work_entries() | ||
159 | 754 | if tags: | ||
160 | 755 | self._report_tags(output, tags) | ||
161 | 756 | |||
162 | 707 | def weekly_report_categorized(self, output, email, who, | 757 | def weekly_report_categorized(self, output, email, who, |
163 | 708 | estimated_column=False): | 758 | estimated_column=False): |
164 | 709 | """Format a weekly report with entries displayed under categories.""" | 759 | """Format a weekly report with entries displayed under categories.""" |
165 | @@ -766,27 +816,24 @@ | |||
166 | 766 | print >> output | 816 | print >> output |
167 | 767 | work, slack = window.grouped_entries() | 817 | work, slack = window.grouped_entries() |
168 | 768 | total_work, total_slacking = window.totals() | 818 | total_work, total_slacking = window.totals() |
169 | 769 | categories = {} | ||
170 | 770 | if work: | 819 | if work: |
171 | 771 | for start, entry, duration in work: | 820 | for start, entry, duration in work: |
172 | 772 | entry = entry[:1].upper() + entry[1:] | 821 | entry = entry[:1].upper() + entry[1:] |
173 | 773 | print >> output, u"%-62s %s" % (entry, | 822 | print >> output, u"%-62s %s" % (entry, |
174 | 774 | format_duration_long(duration)) | 823 | format_duration_long(duration)) |
175 | 775 | if ': ' in entry: | ||
176 | 776 | cat, task = entry.split(': ', 1) | ||
177 | 777 | categories[cat] = categories.get( | ||
178 | 778 | cat, datetime.timedelta(0)) + duration | ||
179 | 779 | else: | ||
180 | 780 | categories[None] = categories.get( | ||
181 | 781 | None, datetime.timedelta(0)) + duration | ||
182 | 782 | 824 | ||
183 | 783 | print >> output | 825 | print >> output |
184 | 784 | print >> output, ("Total work done: %s" % | 826 | print >> output, ("Total work done: %s" % |
185 | 785 | format_duration_long(total_work)) | 827 | format_duration_long(total_work)) |
186 | 786 | 828 | ||
188 | 787 | if len(categories) > 0: | 829 | entries, categories = window.categorized_work_entries() |
189 | 830 | if categories: | ||
190 | 788 | self._report_categories(output, categories) | 831 | self._report_categories(output, categories) |
191 | 789 | 832 | ||
192 | 833 | entries, tags = window.tagged_work_entries() | ||
193 | 834 | if tags: | ||
194 | 835 | self._report_tags(output, tags) | ||
195 | 836 | |||
196 | 790 | print >> output, 'Slacking:\n' | 837 | print >> output, 'Slacking:\n' |
197 | 791 | 838 | ||
198 | 792 | if slack: | 839 | if slack: |
199 | @@ -1053,6 +1100,7 @@ | |||
200 | 1053 | spreadsheet = 'xdg-open %s' | 1100 | spreadsheet = 'xdg-open %s' |
201 | 1054 | chronological = True | 1101 | chronological = True |
202 | 1055 | summary_view = False | 1102 | summary_view = False |
203 | 1103 | tags_view = False | ||
204 | 1056 | show_tasks = True | 1104 | show_tasks = True |
205 | 1057 | 1105 | ||
206 | 1058 | enable_gtk_completion = True # False enables gvim-style completion | 1106 | enable_gtk_completion = True # False enables gvim-style completion |
207 | @@ -1111,6 +1159,7 @@ | |||
208 | 1111 | config.set('gtimelog', 'spreadsheet', self.spreadsheet) | 1159 | config.set('gtimelog', 'spreadsheet', self.spreadsheet) |
209 | 1112 | config.set('gtimelog', 'chronological', str(self.chronological)) | 1160 | config.set('gtimelog', 'chronological', str(self.chronological)) |
210 | 1113 | config.set('gtimelog', 'summary_view', str(self.summary_view)) | 1161 | config.set('gtimelog', 'summary_view', str(self.summary_view)) |
211 | 1162 | config.set('gtimelog', 'tags_view', str(self.tags_view)) | ||
212 | 1114 | config.set('gtimelog', 'show_tasks', str(self.show_tasks)) | 1163 | config.set('gtimelog', 'show_tasks', str(self.show_tasks)) |
213 | 1115 | config.set('gtimelog', 'gtk-completion', | 1164 | config.set('gtimelog', 'gtk-completion', |
214 | 1116 | str(self.enable_gtk_completion)) | 1165 | str(self.enable_gtk_completion)) |
215 | @@ -1140,6 +1189,7 @@ | |||
216 | 1140 | self.spreadsheet = config.get('gtimelog', 'spreadsheet') | 1189 | self.spreadsheet = config.get('gtimelog', 'spreadsheet') |
217 | 1141 | self.chronological = config.getboolean('gtimelog', 'chronological') | 1190 | self.chronological = config.getboolean('gtimelog', 'chronological') |
218 | 1142 | self.summary_view = config.getboolean('gtimelog', 'summary_view') | 1191 | self.summary_view = config.getboolean('gtimelog', 'summary_view') |
219 | 1192 | self.tags_view = config.getboolean('gtimelog', 'tags_view') | ||
220 | 1143 | self.show_tasks = config.getboolean('gtimelog', 'show_tasks') | 1193 | self.show_tasks = config.getboolean('gtimelog', 'show_tasks') |
221 | 1144 | self.enable_gtk_completion = config.getboolean('gtimelog', | 1194 | self.enable_gtk_completion = config.getboolean('gtimelog', |
222 | 1145 | 'gtk-completion') | 1195 | 'gtk-completion') |
223 | @@ -1440,6 +1490,7 @@ | |||
224 | 1440 | self.chronological = (settings.chronological | 1490 | self.chronological = (settings.chronological |
225 | 1441 | and not settings.summary_view) | 1491 | and not settings.summary_view) |
226 | 1442 | self.summary_view = settings.summary_view | 1492 | self.summary_view = settings.summary_view |
227 | 1493 | self.tags_view = settings.tags_view | ||
228 | 1443 | self.show_tasks = settings.show_tasks | 1494 | self.show_tasks = settings.show_tasks |
229 | 1444 | self.looking_at_date = None | 1495 | self.looking_at_date = None |
230 | 1445 | self.entry_watchers = [] | 1496 | self.entry_watchers = [] |
231 | @@ -1454,6 +1505,8 @@ | |||
232 | 1454 | chronological_menu_item.set_active(self.chronological) | 1505 | chronological_menu_item.set_active(self.chronological) |
233 | 1455 | summary_menu_item = builder.get_object('summary') | 1506 | summary_menu_item = builder.get_object('summary') |
234 | 1456 | summary_menu_item.set_active(self.summary_view) | 1507 | summary_menu_item.set_active(self.summary_view) |
235 | 1508 | tags_menu_item = builder.get_object('tags') | ||
236 | 1509 | tags_menu_item.set_active(self.tags_view) | ||
237 | 1457 | show_task_pane_item = builder.get_object('show_task_pane') | 1510 | show_task_pane_item = builder.get_object('show_task_pane') |
238 | 1458 | show_task_pane_item.set_active(self.show_tasks) | 1511 | show_task_pane_item.set_active(self.show_tasks) |
239 | 1459 | # Now hook up signals. | 1512 | # Now hook up signals. |
240 | @@ -1568,6 +1621,13 @@ | |||
241 | 1568 | where = buffer.get_end_iter() | 1621 | where = buffer.get_end_iter() |
242 | 1569 | where.backward_cursor_position() | 1622 | where.backward_cursor_position() |
243 | 1570 | buffer.place_cursor(where) | 1623 | buffer.place_cursor(where) |
244 | 1624 | elif self.tags_view: | ||
245 | 1625 | entries, totals = window.tagged_work_entries() | ||
246 | 1626 | for category, duration in sorted(totals.items()): | ||
247 | 1627 | self.write_group(category or 'no category', duration) | ||
248 | 1628 | where = buffer.get_end_iter() | ||
249 | 1629 | where.backward_cursor_position() | ||
250 | 1630 | buffer.place_cursor(where) | ||
251 | 1571 | else: | 1631 | else: |
252 | 1572 | work, slack = window.grouped_entries() | 1632 | work, slack = window.grouped_entries() |
253 | 1573 | for start, entry, duration in work + slack: | 1633 | for start, entry, duration in work + slack: |
254 | @@ -1830,18 +1890,28 @@ | |||
255 | 1830 | """View -> Chronological""" | 1890 | """View -> Chronological""" |
256 | 1831 | self.chronological = True | 1891 | self.chronological = True |
257 | 1832 | self.summary_view = False | 1892 | self.summary_view = False |
258 | 1893 | self.tags_view = False | ||
259 | 1833 | self.populate_log() | 1894 | self.populate_log() |
260 | 1834 | 1895 | ||
261 | 1835 | def on_grouped_activate(self, widget): | 1896 | def on_grouped_activate(self, widget): |
262 | 1836 | """View -> Grouped""" | 1897 | """View -> Grouped""" |
263 | 1837 | self.chronological = False | 1898 | self.chronological = False |
264 | 1838 | self.summary_view = False | 1899 | self.summary_view = False |
265 | 1900 | self.tags_view = False | ||
266 | 1839 | self.populate_log() | 1901 | self.populate_log() |
267 | 1840 | 1902 | ||
268 | 1841 | def on_summary_activate(self, widget): | 1903 | def on_summary_activate(self, widget): |
269 | 1842 | """View -> Summary""" | 1904 | """View -> Summary""" |
270 | 1843 | self.chronological = False | 1905 | self.chronological = False |
271 | 1844 | self.summary_view = True | 1906 | self.summary_view = True |
272 | 1907 | self.tags_view = False | ||
273 | 1908 | self.populate_log() | ||
274 | 1909 | |||
275 | 1910 | def on_tags_activate(self, widget): | ||
276 | 1911 | """View -> Tags""" | ||
277 | 1912 | self.chronological = False | ||
278 | 1913 | self.summary_view = False | ||
279 | 1914 | self.tags_view = True | ||
280 | 1845 | self.populate_log() | 1915 | self.populate_log() |
281 | 1846 | 1916 | ||
282 | 1847 | def daily_window(self, day=None): | 1917 | def daily_window(self, day=None): |