Merge lp:~kevin-mehall/gtg/hamster-plugin into lp:~gtg/gtg/old-trunk
- hamster-plugin
- Merge into old-trunk
Proposed by
Kevin Mehall
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Paulo Cabido | ||||||||
Approved revision: | 309 | ||||||||
Merged at revision: | not available | ||||||||
Proposed branch: | lp:~kevin-mehall/gtg/hamster-plugin | ||||||||
Merge into: | lp:~gtg/gtg/old-trunk | ||||||||
Diff against target: | None lines | ||||||||
To merge this branch: | bzr merge lp:~kevin-mehall/gtg/hamster-plugin | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paulo Cabido (community) | Approve | ||
Review via email: mp+9516@code.launchpad.net |
Commit message
Merge with Kevin Mehall's hamster-plugin branch
Description of the change
To post a comment you must log in.
Revision history for this message
Kevin Mehall (kevin-mehall) wrote : | # |
- 308. By Kevin Mehall
-
Fix code that chooses Hamster activity
Revision history for this message
Kevin Mehall (kevin-mehall) wrote : | # |
> Also fixes bug #207291
Typo: should be bug https:/
- 309. By Kevin Mehall
-
plugin api suggestions from pcabido
Revision history for this message
Paulo Cabido (pcabido) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'GTG/core/plugins/api.py' | |||
2 | --- GTG/core/plugins/api.py 2009-07-29 18:00:00 +0000 | |||
3 | +++ GTG/core/plugins/api.py 2009-07-30 20:28:53 +0000 | |||
4 | @@ -88,6 +88,13 @@ | |||
5 | 88 | except Exception, e: | 88 | except Exception, e: |
6 | 89 | print "Error adding a toolbar item in to the TaskEditor: %s" % e | 89 | print "Error adding a toolbar item in to the TaskEditor: %s" % e |
7 | 90 | 90 | ||
8 | 91 | def add_task_window_region(self, widget): | ||
9 | 92 | "Adds a widget to the bottom of the task editor dialog" | ||
10 | 93 | v = self.__wTree.get_widget('vbox4') | ||
11 | 94 | v.pack_start(widget) | ||
12 | 95 | v.reorder_child(widget, -2) | ||
13 | 96 | widget.show_all() | ||
14 | 97 | |||
15 | 91 | # passes the requester to the plugin | 98 | # passes the requester to the plugin |
16 | 92 | def get_requester(self): | 99 | def get_requester(self): |
17 | 93 | return self.__requester | 100 | return self.__requester |
18 | @@ -182,4 +189,4 @@ | |||
19 | 182 | # add's a tid to the workview filter | 189 | # add's a tid to the workview filter |
20 | 183 | def add_task_to_workview_filter(self, tid): | 190 | def add_task_to_workview_filter(self, tid): |
21 | 184 | self.__workview_task_filter.append(tid) | 191 | self.__workview_task_filter.append(tid) |
22 | 185 | |||
23 | 186 | \ No newline at end of file | 192 | \ No newline at end of file |
24 | 193 | |||
25 | 187 | 194 | ||
26 | === modified file 'GTG/core/task.py' | |||
27 | --- GTG/core/task.py 2009-07-30 12:15:24 +0000 | |||
28 | +++ GTG/core/task.py 2009-07-30 20:25:17 +0000 | |||
29 | @@ -50,6 +50,7 @@ | |||
30 | 50 | self.loaded = newtask | 50 | self.loaded = newtask |
31 | 51 | if self.loaded : | 51 | if self.loaded : |
32 | 52 | self.req._task_loaded(self.tid) | 52 | self.req._task_loaded(self.tid) |
33 | 53 | self.attributes={} | ||
34 | 53 | 54 | ||
35 | 54 | def is_loaded(self) : | 55 | def is_loaded(self) : |
36 | 55 | return self.loaded | 56 | return self.loaded |
37 | @@ -366,6 +367,24 @@ | |||
38 | 366 | else : | 367 | else : |
39 | 367 | to_return = len(self.parents)!=0 | 368 | to_return = len(self.parents)!=0 |
40 | 368 | return to_return | 369 | return to_return |
41 | 370 | |||
42 | 371 | def set_attribute(self, att_name, att_value, namespace=""): | ||
43 | 372 | """Set an arbitrary attribute. | ||
44 | 373 | |||
45 | 374 | @param att_name: The name of the attribute. | ||
46 | 375 | @param att_value: The value of the attribute. Will be converted to a | ||
47 | 376 | string. | ||
48 | 377 | """ | ||
49 | 378 | val = unicode(str(att_value), "UTF-8") | ||
50 | 379 | self.attributes[(namespace,att_name)] = val | ||
51 | 380 | self.sync() | ||
52 | 381 | |||
53 | 382 | def get_attribute(self, att_name, namespace=""): | ||
54 | 383 | """Get the attribute C{att_name}. | ||
55 | 384 | |||
56 | 385 | Returns C{None} if there is no attribute matching C{att_name}. | ||
57 | 386 | """ | ||
58 | 387 | return self.attributes.get((namespace,att_name), None) | ||
59 | 369 | 388 | ||
60 | 370 | #Method called before the task is deleted | 389 | #Method called before the task is deleted |
61 | 371 | #This method is called by the datastore and should not be called directly | 390 | #This method is called by the datastore and should not be called directly |
62 | 372 | 391 | ||
63 | === modified file 'GTG/plugins/hamster/hamster.py' | |||
64 | --- GTG/plugins/hamster/hamster.py 2009-07-30 15:43:14 +0000 | |||
65 | +++ GTG/plugins/hamster/hamster.py 2009-07-31 15:34:47 +0000 | |||
66 | @@ -19,15 +19,20 @@ | |||
67 | 19 | import gtk, pygtk | 19 | import gtk, pygtk |
68 | 20 | import os | 20 | import os |
69 | 21 | import dbus | 21 | import dbus |
70 | 22 | import time | ||
71 | 23 | from calendar import timegm | ||
72 | 22 | 24 | ||
73 | 23 | class hamsterPlugin: | 25 | class hamsterPlugin: |
74 | 24 | PLUGIN_NAME = 'Hamster Time Tracker Integration' | 26 | PLUGIN_NAME = 'Hamster Time Tracker Integration' |
75 | 25 | PLUGIN_AUTHORS = 'Kevin Mehall <km@kevinmehall.net>' | 27 | PLUGIN_AUTHORS = 'Kevin Mehall <km@kevinmehall.net>' |
77 | 26 | PLUGIN_VERSION = '0.1' | 28 | PLUGIN_VERSION = '0.2' |
78 | 27 | PLUGIN_DESCRIPTION = 'Adds the ability to send a task to the Hamster time tracking applet' | 29 | PLUGIN_DESCRIPTION = 'Adds the ability to send a task to the Hamster time tracking applet' |
79 | 28 | PLUGIN_ENABLED = False | 30 | PLUGIN_ENABLED = False |
81 | 29 | 31 | PLUGIN_NAMESPACE = 'hamster-plugin' | |
82 | 32 | |||
83 | 33 | #### Interaction with Hamster | ||
84 | 30 | def sendTask(self, task): | 34 | def sendTask(self, task): |
85 | 35 | """Send a gtg task to hamster-applet""" | ||
86 | 31 | if task is None: return | 36 | if task is None: return |
87 | 32 | title=task.get_title() | 37 | title=task.get_title() |
88 | 33 | tags=task.get_tags_name() | 38 | tags=task.get_tags_name() |
89 | @@ -39,22 +44,48 @@ | |||
90 | 39 | if len(activity_candidates)>=1: | 44 | if len(activity_candidates)>=1: |
91 | 40 | activity=list(activity_candidates)[0] | 45 | activity=list(activity_candidates)[0] |
92 | 41 | #TODO: if >1, how to choose best one? | 46 | #TODO: if >1, how to choose best one? |
94 | 42 | else: | 47 | elif len(activity_candidates)>0: |
95 | 43 | #TODO: is there anything more reasonable that can be done? | 48 | #TODO: is there anything more reasonable that can be done? |
96 | 44 | activity=tags[0] | 49 | activity=tags[0] |
97 | 50 | else: | ||
98 | 51 | activity = "Other" | ||
99 | 45 | 52 | ||
111 | 46 | self.hamster.AddFact('%s,%s'%(activity, title), 0, 0) | 53 | hamster_id=self.hamster.AddFact('%s,%s'%(activity, title), 0, 0) |
112 | 47 | 54 | ||
113 | 48 | def hamsterError(self): | 55 | ids=self.get_hamster_ids(task) |
114 | 49 | d=gtk.MessageDialog(buttons=gtk.BUTTONS_CANCEL) | 56 | ids.append(str(hamster_id)) |
115 | 50 | d.set_markup("<big>Error loading plugin</big>") | 57 | self.set_hamster_ids(task, ids) |
116 | 51 | d.format_secondary_markup("This plugin requires hamster-applet 2.27.3 or greater\n\ | 58 | |
117 | 52 | Please install hamster-applet and make sure the applet is added to the panel") | 59 | def get_records(self, task): |
118 | 53 | d.run() | 60 | """Get a list of hamster facts for a task""" |
119 | 54 | d.destroy() | 61 | ids = self.get_hamster_ids(task) |
120 | 55 | 62 | records=[] | |
121 | 56 | # plugin engine methods | 63 | modified=False |
122 | 64 | valid_ids=[] | ||
123 | 65 | for i in ids: | ||
124 | 66 | d=self.hamster.GetFactById(i) | ||
125 | 67 | if d.get("id", None): # check if fact still exists | ||
126 | 68 | records.append(d) | ||
127 | 69 | valid_ids.append(i) | ||
128 | 70 | else: | ||
129 | 71 | modified=True | ||
130 | 72 | print "Removing invalid fact", i | ||
131 | 73 | if modified: | ||
132 | 74 | self.set_hamster_ids(task, valid_ids) | ||
133 | 75 | return records | ||
134 | 76 | |||
135 | 77 | #### Datastore | ||
136 | 78 | def get_hamster_ids(self, task): | ||
137 | 79 | a = task.get_attribute("id-list", namespace=self.PLUGIN_NAMESPACE) | ||
138 | 80 | if not a: return [] | ||
139 | 81 | else: return a.split(',') | ||
140 | 82 | |||
141 | 83 | def set_hamster_ids(self, task, ids): | ||
142 | 84 | task.set_attribute("id-list", ",".join(ids), namespace=self.PLUGIN_NAMESPACE) | ||
143 | 85 | |||
144 | 86 | #### Plugin api methods | ||
145 | 57 | def activate(self, plugin_api): | 87 | def activate(self, plugin_api): |
146 | 88 | # connect to hamster-applet | ||
147 | 58 | try: | 89 | try: |
148 | 59 | self.hamster=dbus.SessionBus().get_object('org.gnome.Hamster', '/org/gnome/Hamster') | 90 | self.hamster=dbus.SessionBus().get_object('org.gnome.Hamster', '/org/gnome/Hamster') |
149 | 60 | self.hamster.GetActivities() | 91 | self.hamster.GetActivities() |
150 | @@ -62,25 +93,22 @@ | |||
151 | 62 | self.hamsterError() | 93 | self.hamsterError() |
152 | 63 | return False | 94 | return False |
153 | 64 | 95 | ||
154 | 96 | # add menu item | ||
155 | 65 | self.menu_item = gtk.MenuItem("Start task in Hamster") | 97 | self.menu_item = gtk.MenuItem("Start task in Hamster") |
156 | 66 | self.menu_item.connect('activate', self.browser_cb, plugin_api) | 98 | self.menu_item.connect('activate', self.browser_cb, plugin_api) |
157 | 99 | plugin_api.add_menu_item(self.menu_item) | ||
158 | 67 | 100 | ||
159 | 101 | # and button | ||
160 | 68 | self.button=gtk.ToolButton() | 102 | self.button=gtk.ToolButton() |
161 | 69 | self.button.set_label("Start") | 103 | self.button.set_label("Start") |
162 | 70 | self.button.set_icon_name('hamster-applet') | 104 | self.button.set_icon_name('hamster-applet') |
163 | 71 | self.button.set_tooltip_text("Start a new activity in Hamster Time Tracker based on the selected task") | 105 | self.button.set_tooltip_text("Start a new activity in Hamster Time Tracker based on the selected task") |
164 | 72 | self.button.connect('clicked', self.browser_cb, plugin_api) | 106 | self.button.connect('clicked', self.browser_cb, plugin_api) |
172 | 73 | 107 | self.separator = plugin_api.add_toolbar_item(gtk.SeparatorToolItem()) # saves the separator's index to later remove it | |
166 | 74 | # add a menu item to the menu bar | ||
167 | 75 | plugin_api.add_menu_item(self.menu_item) | ||
168 | 76 | |||
169 | 77 | # saves the separator's index to later remove it | ||
170 | 78 | self.separator = plugin_api.add_toolbar_item(gtk.SeparatorToolItem()) | ||
171 | 79 | # add a item (button) to the ToolBar | ||
173 | 80 | plugin_api.add_toolbar_item(self.button) | 108 | plugin_api.add_toolbar_item(self.button) |
174 | 81 | 109 | ||
175 | 82 | def onTaskOpened(self, plugin_api): | 110 | def onTaskOpened(self, plugin_api): |
177 | 83 | # add a item (button) to the ToolBar | 111 | # add button |
178 | 84 | self.taskbutton = gtk.ToolButton() | 112 | self.taskbutton = gtk.ToolButton() |
179 | 85 | self.taskbutton.set_label("Start") | 113 | self.taskbutton.set_label("Start") |
180 | 86 | self.taskbutton.set_icon_name('hamster-applet') | 114 | self.taskbutton.set_icon_name('hamster-applet') |
181 | @@ -89,6 +117,54 @@ | |||
182 | 89 | plugin_api.add_task_toolbar_item(gtk.SeparatorToolItem()) | 117 | plugin_api.add_task_toolbar_item(gtk.SeparatorToolItem()) |
183 | 90 | plugin_api.add_task_toolbar_item(self.taskbutton) | 118 | plugin_api.add_task_toolbar_item(self.taskbutton) |
184 | 91 | 119 | ||
185 | 120 | task = plugin_api.get_task() | ||
186 | 121 | records = self.get_records(task) | ||
187 | 122 | |||
188 | 123 | if len(records): | ||
189 | 124 | # add section to bottom of window | ||
190 | 125 | vbox = gtk.VBox() | ||
191 | 126 | inner_table = gtk.Table(rows=len(records), columns=2) | ||
192 | 127 | if len(records)>8: | ||
193 | 128 | s = gtk.ScrolledWindow() | ||
194 | 129 | s.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) | ||
195 | 130 | v=gtk.Viewport() | ||
196 | 131 | v.add(inner_table) | ||
197 | 132 | s.add(v) | ||
198 | 133 | v.set_shadow_type(gtk.SHADOW_NONE) | ||
199 | 134 | s.set_size_request(-1, 150) | ||
200 | 135 | else: | ||
201 | 136 | s=inner_table | ||
202 | 137 | |||
203 | 138 | outer_table = gtk.Table(rows=1, columns=2) | ||
204 | 139 | vbox.pack_start(s) | ||
205 | 140 | vbox.pack_start(outer_table) | ||
206 | 141 | vbox.pack_end(gtk.HSeparator()) | ||
207 | 142 | |||
208 | 143 | total = 0 | ||
209 | 144 | |||
210 | 145 | def add(w, a, b, offset): | ||
211 | 146 | dateLabel=gtk.Label(a) | ||
212 | 147 | dateLabel.set_use_markup(True) | ||
213 | 148 | dateLabel.set_alignment(xalign=0.0, yalign=0.5) | ||
214 | 149 | dateLabel.set_size_request(200, -1) | ||
215 | 150 | w.attach(dateLabel, left_attach=0, right_attach=1, top_attach=offset, | ||
216 | 151 | bottom_attach=offset+1, xoptions=gtk.FILL, xpadding=20, yoptions=0) | ||
217 | 152 | |||
218 | 153 | durLabel=gtk.Label(b) | ||
219 | 154 | durLabel.set_use_markup(True) | ||
220 | 155 | durLabel.set_alignment(xalign=0.0, yalign=0.5) | ||
221 | 156 | w.attach(durLabel, left_attach=1, right_attach=2, top_attach=offset, | ||
222 | 157 | bottom_attach=offset+1, xoptions=gtk.FILL, yoptions=0) | ||
223 | 158 | |||
224 | 159 | for offset,i in enumerate(records): | ||
225 | 160 | t = calc_duration(i) | ||
226 | 161 | total += t | ||
227 | 162 | add(inner_table, format_date(i), format_duration(t), offset) | ||
228 | 163 | |||
229 | 164 | add(outer_table, "<big><b>Total</b></big>", "<big><b>%s</b></big>"%format_duration(total), 1) | ||
230 | 165 | |||
231 | 166 | plugin_api.add_task_window_region(vbox) | ||
232 | 167 | |||
233 | 92 | def deactivate(self, plugin_api): | 168 | def deactivate(self, plugin_api): |
234 | 93 | plugin_api.remove_menu_item(self.menu_item) | 169 | plugin_api.remove_menu_item(self.menu_item) |
235 | 94 | plugin_api.remove_toolbar_item(self.button) | 170 | plugin_api.remove_toolbar_item(self.button) |
236 | @@ -99,5 +175,48 @@ | |||
237 | 99 | 175 | ||
238 | 100 | def task_cb(self, widget, plugin_api): | 176 | def task_cb(self, widget, plugin_api): |
239 | 101 | self.sendTask(plugin_api.get_task()) | 177 | self.sendTask(plugin_api.get_task()) |
242 | 102 | 178 | ||
243 | 103 | 179 | def hamsterError(self): | |
244 | 180 | """Display error dialog""" | ||
245 | 181 | d=gtk.MessageDialog(buttons=gtk.BUTTONS_CANCEL) | ||
246 | 182 | d.set_markup("<big>Error loading plugin</big>") | ||
247 | 183 | d.format_secondary_markup("This plugin requires hamster-applet 2.27.3 or greater\n\ | ||
248 | 184 | Please install hamster-applet and make sure the applet is added to the panel") | ||
249 | 185 | d.run() | ||
250 | 186 | d.destroy() | ||
251 | 187 | |||
252 | 188 | #### Helper Functions | ||
253 | 189 | def format_date(task): | ||
254 | 190 | return time.strftime("<b>%A, %b %e</b> %l:%M %p", time.gmtime(task['start_time'])) | ||
255 | 191 | |||
256 | 192 | def calc_duration(fact): | ||
257 | 193 | start=fact['start_time'] | ||
258 | 194 | end=fact['end_time'] | ||
259 | 195 | if not end: end=timegm(time.localtime()) | ||
260 | 196 | return end-start | ||
261 | 197 | |||
262 | 198 | def format_duration(seconds): | ||
263 | 199 | # Based on hamster-applet code - hamster/stuff.py | ||
264 | 200 | """formats duration in a human readable format.""" | ||
265 | 201 | |||
266 | 202 | minutes = seconds / 60 | ||
267 | 203 | |||
268 | 204 | if not minutes: | ||
269 | 205 | return "0min" | ||
270 | 206 | |||
271 | 207 | hours = minutes / 60 | ||
272 | 208 | minutes = minutes % 60 | ||
273 | 209 | formatted_duration = "" | ||
274 | 210 | |||
275 | 211 | if minutes % 60 == 0: | ||
276 | 212 | # duration in round hours | ||
277 | 213 | formatted_duration += "%dh" % (hours) | ||
278 | 214 | elif hours == 0: | ||
279 | 215 | # duration less than hour | ||
280 | 216 | formatted_duration += "%dmin" % (minutes % 60.0) | ||
281 | 217 | else: | ||
282 | 218 | # x hours, y minutes | ||
283 | 219 | formatted_duration += "%dh %dmin" % (hours, minutes % 60) | ||
284 | 220 | |||
285 | 221 | return formatted_duration | ||
286 | 222 | |||
287 | 104 | 223 | ||
288 | === modified file 'GTG/tools/taskxml.py' | |||
289 | --- GTG/tools/taskxml.py 2009-03-01 14:09:12 +0000 | |||
290 | +++ GTG/tools/taskxml.py 2009-07-31 15:52:52 +0000 | |||
291 | @@ -35,6 +35,15 @@ | |||
292 | 35 | for s in sub_list : | 35 | for s in sub_list : |
293 | 36 | sub_tid = s.childNodes[0].nodeValue | 36 | sub_tid = s.childNodes[0].nodeValue |
294 | 37 | cur_task.add_subtask(sub_tid) | 37 | cur_task.add_subtask(sub_tid) |
295 | 38 | attr_list = xmlnode.getElementsByTagName("attribute") | ||
296 | 39 | for a in attr_list: | ||
297 | 40 | if len(a.childNodes): | ||
298 | 41 | content = a.childNodes[0].nodeValue | ||
299 | 42 | else: | ||
300 | 43 | content = "" | ||
301 | 44 | key = a.getAttribute("key") | ||
302 | 45 | namespace = a.getAttribute("namespace") | ||
303 | 46 | cur_task.set_attribute(key, content, namespace=namespace) | ||
304 | 38 | tasktext = xmlnode.getElementsByTagName("content") | 47 | tasktext = xmlnode.getElementsByTagName("content") |
305 | 39 | if len(tasktext) > 0 : | 48 | if len(tasktext) > 0 : |
306 | 40 | if tasktext[0].firstChild : | 49 | if tasktext[0].firstChild : |
307 | @@ -67,6 +76,14 @@ | |||
308 | 67 | childs = task.get_subtasks_tid() | 76 | childs = task.get_subtasks_tid() |
309 | 68 | for c in childs : | 77 | for c in childs : |
310 | 69 | cleanxml.addTextNode(doc,t_xml,"subtask",c) | 78 | cleanxml.addTextNode(doc,t_xml,"subtask",c) |
311 | 79 | for a in task.attributes: | ||
312 | 80 | namespace,key=a | ||
313 | 81 | content=task.attributes[a] | ||
314 | 82 | element = doc.createElement('attribute') | ||
315 | 83 | element.setAttribute("namespace", namespace) | ||
316 | 84 | element.setAttribute("key", key) | ||
317 | 85 | element.appendChild(doc.createTextNode(content)) | ||
318 | 86 | t_xml.appendChild(element) | ||
319 | 70 | tex = task.get_text() | 87 | tex = task.get_text() |
320 | 71 | if tex : | 88 | if tex : |
321 | 72 | #We take the xml text and convert it to a string | 89 | #We take the xml text and convert it to a string |
Added the ability to display related Hamster activities and time totals at the bottom of the task window. This required 2 changes to GTG core:
- Add the ability to save arbitrary attributes with tasks
- Add a function to the plugin API that adds a widget to the bottom of the tasks window (pcabido, please review this)
Also fixes bug #207291