GTG

Merge lp:~kevin-mehall/gtg/hamster-plugin into lp:~gtg/gtg/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
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

To post a comment you must log in.
Revision history for this message
Kevin Mehall (kevin-mehall) wrote :

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

lp:~kevin-mehall/gtg/hamster-plugin updated
308. By Kevin Mehall

Fix code that chooses Hamster activity

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

Subscribers

People subscribed via source and target branches

to status/vote changes: