GTG

Merge lp://staging/~bryce/gtg/import_json into lp://staging/~gtg/gtg/old-trunk

Proposed by Bryce Harrington
Status: Merged
Merged at revision: not available
Proposed branch: lp://staging/~bryce/gtg/import_json
Merge into: lp://staging/~gtg/gtg/old-trunk
Diff against target: 453 lines (+433/-0)
4 files modified
GTG/plugins/import_json.gtg-plugin (+8/-0)
GTG/plugins/import_json/__init__.py (+25/-0)
GTG/plugins/import_json/import_json.glade (+178/-0)
GTG/plugins/import_json/import_json.py (+222/-0)
To merge this branch: bzr merge lp://staging/~bryce/gtg/import_json
Reviewer Review Type Date Requested Status
Luca Invernizzi (community) Needs Resubmitting
Bertrand Rousseau (community) Approve
Review via email: mp+18823@code.staging.launchpad.net
To post a comment you must log in.
Revision history for this message
Bryce Harrington (bryce) wrote :

This adds a plugin for importing tasks from json formatted files.

Revision history for this message
Luca Invernizzi (invernizzi) wrote :

Bryce, could you provide a sample of a json formatted file for tasks (so that we can try it)?
How are you planning to use this?

review: Needs Information
Revision history for this message
Bryce Harrington (bryce) wrote :

No problem. Example json files can be found at:
  http://people.canonical.com/~pitti/workitems/

The planned use for this is as follows. In Ubuntu each release cycle the Ubuntu developers write the feature development plans as "blueprints". Each blueprint has a set of tasks associated with them listed in its whiteboard, which are assigned to specific people. However, often an individual is assigned tasks on multiple different blueprints, which can be tough to keep track of. Fortunately, the tasks are published in a JSON file format at the above address, which this plugin downloads and parses.

[Obviously it would be most useful if this could also update the statuses of the blueprint tasks, but unfortunately that is a bit trickier since there is not an API to manipulate tasks in Launchpad (at least, not yet).]

An example blueprint with the tasks itemized:
https://blueprints.edge.launchpad.net/ubuntu/+spec/desktop-lucid-xorg-driver-selection-for-nvidia-hardware

Revision history for this message
Bertrand Rousseau (bertrand-rousseau) wrote :

I'm alright with this. But I guess once we'll have our "backend" feature, you should consider writing this plugin for it.

review: Approve
Revision history for this message
Luca Invernizzi (invernizzi) wrote :

Agreed (We should really have a guide for gtg).

review: Approve
Revision history for this message
Luca Invernizzi (invernizzi) wrote :

Arg. We don't use glade anymore (gtk-builder instead).
Since i'm doing little fixes to follow the other plugins standard (and setup.py...), I'll fix that.

Revision history for this message
Luca Invernizzi (invernizzi) wrote :

Actually, it will need some changes in the function signatures, so it's better if you finish the conversion to gtk-builder yourself. follow the helloworld plugin.

I made some changed that you should merge: lp:~gtg-user/gtg/import_json
basically I've updated setup.py and MANIFEST.in to include your plugin in the package, added the gtk-builder interface file and added the dependencies in the plugin description.

review: Needs Resubmitting

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'GTG/plugins/import_json'
2=== added file 'GTG/plugins/import_json.gtg-plugin'
3--- GTG/plugins/import_json.gtg-plugin 1970-01-01 00:00:00 +0000
4+++ GTG/plugins/import_json.gtg-plugin 2010-02-08 05:30:27 +0000
5@@ -0,0 +1,8 @@
6+[GTG Plugin]
7+Module=import_json
8+Name=Import from JSON
9+Description='''Imports tasks from JSON formatted files.
10+'''
11+Authors=Bryce Harrington <bryce@canonical.com>
12+Version=0.1
13+Enabled=False
14
15=== added file 'GTG/plugins/import_json/__init__.py'
16--- GTG/plugins/import_json/__init__.py 1970-01-01 00:00:00 +0000
17+++ GTG/plugins/import_json/__init__.py 2010-02-08 05:30:27 +0000
18@@ -0,0 +1,25 @@
19+# -*- coding: utf-8 -*-
20+# Copyright (c) 2009 - Paulo Cabido <paulo.cabido@gmail.com>
21+#
22+# This program is free software: you can redistribute it and/or modify it under
23+# the terms of the GNU General Public License as published by the Free Software
24+# Foundation, either version 3 of the License, or (at your option) any later
25+# version.
26+#
27+# This program is distributed in the hope that it will be useful, but WITHOUT
28+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
29+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
30+# details.
31+#
32+# You should have received a copy of the GNU General Public License along with
33+# this program. If not, see <http://www.gnu.org/licenses/>.
34+
35+import sys
36+import os
37+sys.path.insert(0,os.getcwd())
38+
39+from import_json import pluginImportJson
40+
41+
42+#suppress pyflakes warning (given by make lint)
43+if False == True: pluginImportJson()
44
45=== added file 'GTG/plugins/import_json/import_json.glade'
46--- GTG/plugins/import_json/import_json.glade 1970-01-01 00:00:00 +0000
47+++ GTG/plugins/import_json/import_json.glade 2010-02-08 05:30:27 +0000
48@@ -0,0 +1,178 @@
49+<?xml version="1.0"?>
50+<glade-interface>
51+ <!-- interface-requires gtk+ 2.6 -->
52+ <!-- interface-naming-policy toplevel-contextual -->
53+ <widget class="GtkDialog" id="dlg_import_json">
54+ <property name="width_request">300</property>
55+ <property name="title" translatable="yes">Import from JSON</property>
56+ <property name="type_hint">dialog</property>
57+ <property name="has_separator">False</property>
58+ <child internal-child="vbox">
59+ <widget class="GtkVBox" id="dialog-vbox1">
60+ <property name="visible">True</property>
61+ <child>
62+ <widget class="GtkVBox" id="vbox1">
63+ <property name="visible">True</property>
64+ <child>
65+ <widget class="GtkLabel" id="lbl_import_json">
66+ <property name="visible">True</property>
67+ <property name="xalign">0</property>
68+ <property name="yalign">1</property>
69+ <property name="label" translatable="yes">Location to import:</property>
70+ <property name="wrap">True</property>
71+ </widget>
72+ <packing>
73+ <property name="expand">False</property>
74+ <property name="fill">False</property>
75+ <property name="position">0</property>
76+ </packing>
77+ </child>
78+ <child>
79+ <widget class="GtkEntry" id="txt_import">
80+ <property name="visible">True</property>
81+ <property name="can_focus">True</property>
82+ <property name="invisible_char">&#x25CF;</property>
83+ <property name="width_chars">40</property>
84+ </widget>
85+ <packing>
86+ <property name="position">1</property>
87+ </packing>
88+ </child>
89+ <child>
90+ <placeholder/>
91+ </child>
92+ </widget>
93+ <packing>
94+ <property name="position">2</property>
95+ </packing>
96+ </child>
97+ <child internal-child="action_area">
98+ <widget class="GtkHButtonBox" id="dialog-action_area1">
99+ <property name="visible">True</property>
100+ <property name="layout_style">end</property>
101+ <child>
102+ <widget class="GtkButton" id="btn_close">
103+ <property name="label">gtk-close</property>
104+ <property name="response_id">-7</property>
105+ <property name="visible">True</property>
106+ <property name="can_focus">True</property>
107+ <property name="can_default">True</property>
108+ <property name="receives_default">False</property>
109+ <property name="use_stock">True</property>
110+ </widget>
111+ <packing>
112+ <property name="expand">False</property>
113+ <property name="fill">False</property>
114+ <property name="position">0</property>
115+ </packing>
116+ </child>
117+ <child>
118+ <widget class="GtkButton" id="btn_import">
119+ <property name="label" translatable="yes">_Import</property>
120+ <property name="visible">True</property>
121+ <property name="can_focus">True</property>
122+ <property name="receives_default">True</property>
123+ <property name="use_underline">True</property>
124+ </widget>
125+ <packing>
126+ <property name="expand">False</property>
127+ <property name="fill">False</property>
128+ <property name="position">1</property>
129+ </packing>
130+ </child>
131+ </widget>
132+ <packing>
133+ <property name="expand">False</property>
134+ <property name="pack_type">end</property>
135+ <property name="position">0</property>
136+ </packing>
137+ </child>
138+ </widget>
139+ </child>
140+ </widget>
141+ <widget class="GtkDialog" id="dlg_select_username">
142+ <property name="border_width">5</property>
143+ <property name="type_hint">normal</property>
144+ <property name="has_separator">False</property>
145+ <child internal-child="vbox">
146+ <widget class="GtkVBox" id="dialog-vbox2">
147+ <property name="visible">True</property>
148+ <property name="orientation">vertical</property>
149+ <property name="spacing">2</property>
150+ <child>
151+ <widget class="GtkVBox" id="vbox1">
152+ <property name="visible">True</property>
153+ <property name="orientation">vertical</property>
154+ <child>
155+ <widget class="GtkLabel" id="lbl_select_username">
156+ <property name="visible">True</property>
157+ <property name="xalign">0</property>
158+ <property name="yalign">1</property>
159+ <property name="label" translatable="yes">Select username to import tasks from:</property>
160+ </widget>
161+ <packing>
162+ <property name="position">0</property>
163+ </packing>
164+ </child>
165+ <child>
166+ <widget class="GtkComboBoxEntry" id="select_username">
167+ <property name="visible">True</property>
168+ <property name="can_focus">True</property>
169+ <property name="has_focus">True</property>
170+ <property name="tooltip" translatable="yes">Tasks owned by the selected username will be imported.</property>
171+ <property name="items" translatable="yes"></property>
172+ <property name="text_column">0</property>
173+ </widget>
174+ <packing>
175+ <property name="position">1</property>
176+ </packing>
177+ </child>
178+ </widget>
179+ <packing>
180+ <property name="position">1</property>
181+ </packing>
182+ </child>
183+ <child internal-child="action_area">
184+ <widget class="GtkHButtonBox" id="dialog-action_area2">
185+ <property name="visible">True</property>
186+ <property name="layout_style">end</property>
187+ <child>
188+ <widget class="GtkButton" id="btn_cancel">
189+ <property name="label">gtk-cancel</property>
190+ <property name="response_id">-7</property>
191+ <property name="visible">True</property>
192+ <property name="can_focus">True</property>
193+ <property name="receives_default">True</property>
194+ <property name="use_stock">True</property>
195+ </widget>
196+ <packing>
197+ <property name="expand">False</property>
198+ <property name="fill">False</property>
199+ <property name="position">0</property>
200+ </packing>
201+ </child>
202+ <child>
203+ <widget class="GtkButton" id="btn_okay">
204+ <property name="label">gtk-ok</property>
205+ <property name="visible">True</property>
206+ <property name="can_focus">True</property>
207+ <property name="receives_default">True</property>
208+ <property name="use_stock">True</property>
209+ </widget>
210+ <packing>
211+ <property name="expand">False</property>
212+ <property name="fill">False</property>
213+ <property name="position">1</property>
214+ </packing>
215+ </child>
216+ </widget>
217+ <packing>
218+ <property name="expand">False</property>
219+ <property name="pack_type">end</property>
220+ <property name="position">0</property>
221+ </packing>
222+ </child>
223+ </widget>
224+ </child>
225+ </widget>
226+</glade-interface>
227
228=== added file 'GTG/plugins/import_json/import_json.py'
229--- GTG/plugins/import_json/import_json.py 1970-01-01 00:00:00 +0000
230+++ GTG/plugins/import_json/import_json.py 2010-02-08 05:30:27 +0000
231@@ -0,0 +1,222 @@
232+# -*- coding: utf-8 -*-
233+# Copyright (c) 2010 - Bryce Harrington <bryce@canonical.com>
234+#
235+# This program is free software: you can redistribute it and/or modify it under
236+# the terms of the GNU General Public License as published by the Free Software
237+# Foundation, either version 3 of the License, or (at your option) any later
238+# version.
239+#
240+# This program is distributed in the hope that it will be useful, but WITHOUT
241+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
242+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
243+# details.
244+#
245+# You should have received a copy of the GNU General Public License along with
246+# this program. If not, see <http://www.gnu.org/licenses/>.
247+
248+# Imports JSON files with the following syntax:
249+# {
250+# "specs": {
251+# "my-spec": {
252+# ...
253+# "work_items": [
254+# {
255+# "assignee": "john-doe",
256+# "date": "2010-01-23",
257+# "description": "Do something",
258+# "spec": "my-spec",
259+# "status": "todo"
260+# },
261+# ...
262+# ]
263+# },
264+# "another-spec": {
265+# ...
266+# },
267+# ...
268+# },
269+# }
270+
271+import gtk
272+import gobject
273+import os
274+import re
275+import urllib2
276+import simplejson as json
277+
278+class pluginImportJson:
279+
280+ def __init__(self):
281+ self.plugin_api = None
282+
283+ self.menu_item = gtk.MenuItem("Import from _JSON")
284+ self.menu_item.connect('activate', self.on_import_json_activate)
285+
286+ self.tb_button = gtk.ToolButton(gtk.STOCK_INFO)
287+ self.tb_button.set_label("Import from JSON")
288+ self.tb_button.connect('clicked', self.on_import_json_activate)
289+ self.separator = gtk.SeparatorToolItem()
290+
291+ self.dialog = None
292+ self.txtImport = None
293+ self.json_tasks = None
294+
295+ self.dialog_select_username = None
296+ self.select_username = None
297+ self.usernames = []
298+
299+ def activate(self, plugin_api):
300+ self.plugin_api = plugin_api
301+ self.plugin_api.add_menu_item(self.menu_item)
302+ self.plugin_api.add_toolbar_item(self.separator)
303+ self.plugin_api.add_toolbar_item(self.tb_button)
304+
305+ def onTaskClosed(self, plugin_api):
306+ pass
307+
308+ def onTaskOpened(self, plugin_api):
309+ pass
310+
311+ def deactivate(self, plugin_api):
312+ plugin_api.remove_menu_item(self.menu_item)
313+ plugin_api.remove_toolbar_item(self.tb_button)
314+ plugin_api.remove_toolbar_item(self.separator)
315+ self.txtImport = None
316+
317+ def loadDialog(self):
318+ path = os.path.dirname(os.path.abspath(__file__))
319+ glade_file = os.path.join(path, "import_json.glade")
320+ wTree = gtk.glade.XML(glade_file, "dlg_import_json")
321+
322+ self.dialog = wTree.get_widget("dlg_import_json")
323+ if not self.dialog:
324+ return
325+ self.txtImport = wTree.get_widget("txt_import")
326+
327+ self.dialog.connect("delete_event", self.close_dialog)
328+ self.dialog.connect("response", self.on_response)
329+
330+ self.dialog.show_all()
331+
332+ def loadDialogSelectUsername(self):
333+ path = os.path.dirname(os.path.abspath(__file__))
334+ glade_file = os.path.join(path, "import_json.glade")
335+ wTree = gtk.glade.XML(glade_file, "dlg_select_username")
336+
337+ self.dialog_select_username = wTree.get_widget("dlg_select_username")
338+ if not self.dialog_select_username or len(self.usernames) < 1:
339+ return
340+ self.dialog_select_username.set_title("Select username")
341+ self.dialog_select_username.set_transient_for(self.dialog)
342+ self.dialog_select_username.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
343+ # TODO: Handle ok and cancel buttons
344+ self.dialog_select_username.connect("response", self.on_response_select_username)
345+ self.dialog_select_username.connect("delete_event", self.close_dialog_select_username)
346+
347+ self.select_username = wTree.get_widget("select_username")
348+ for u in self.usernames:
349+ self.select_username.append_text(u)
350+ self.select_username.set_active(0)
351+
352+ self.dialog_select_username.show_all()
353+
354+ def print_selected(self, widget, data=None):
355+ print self.select_username.get_active()
356+
357+ def close_dialog(self, widget, data=None):
358+ self.dialog.destroy()
359+ return True
360+
361+ def close_dialog_select_username(self, widget, data=None):
362+ self.dialog_select_username.destroy()
363+ return True
364+
365+ # plugin features
366+ def on_import_json_activate(self, widget):
367+ self.loadDialog()
368+
369+ def on_response(self, widget, response_id):
370+ if response_id == -7 or response_id == -4:
371+ self.close_dialog(widget)
372+ elif response_id == 0 and self.txtImport:
373+ self.import_json(widget)
374+ else:
375+ print "Error: Unknown response id %d" %(response_id)
376+
377+ def on_response_select_username(self, widget, response_id):
378+ if response_id == -7:
379+ self.dialog.show_all()
380+ self.close_dialog_select_username(widget)
381+ elif response_id == -4:
382+ self.close_dialog_select_username(widget)
383+ elif response_id == 0:
384+ self.import_tasks(widget)
385+ self.close_dialog_select_username(widget)
386+ else:
387+ print "Error: Unknown response id %d" %(response_id)
388+ return response_id
389+
390+ def import_json(self, widget):
391+ url = self.txtImport.get_text()
392+ json_text = loadurl(url)
393+ if not json_text:
394+ # TODO: Pop up error dialog
395+ print "Error: Could not load url %s" % url
396+ return
397+
398+ # Convert to json
399+ self.json_tasks = json.loads(json_text)
400+
401+ # TODO: Create listing of usernames available
402+ self.usernames = [ ]
403+ for specname,spec in self.json_tasks['specs'].items():
404+ for wi in spec['work_items']:
405+ if wi['status'] != "todo":
406+ continue
407+ if wi['assignee'] in self.usernames:
408+ continue
409+ if not wi['assignee']:
410+ continue
411+ self.usernames.append(wi['assignee'])
412+ self.usernames.sort()
413+
414+ # Pop up dialog allowing user to select username
415+ self.loadDialogSelectUsername()
416+ self.dialog.hide_all()
417+ self.dialog_select_username.run()
418+
419+ def import_tasks(self, widget):
420+ username = self.usernames[self.select_username.get_active()]
421+ re_dehtml = re.compile(r'<.*?>')
422+
423+ for specname,spec in self.json_tasks['specs'].items():
424+ for wi in spec['work_items']:
425+ if wi['assignee'] != username:
426+ continue
427+ if wi['status'] != 'todo':
428+ continue
429+
430+ text = ""
431+ if spec['details_url']:
432+ text = spec['details_url']
433+ elif spec['url']:
434+ text = spec['url']
435+ task = self.plugin_api.get_requester().new_task(pid=None, tags=None, newtask=True)
436+ task.set_title(re_dehtml.sub('', wi['description']))
437+ task.set_text(re_dehtml.sub('', text))
438+ task.sync()
439+ # TODO: Do something with spec['priority']
440+
441+ self.close_dialog_select_username(widget)
442+ self.close_dialog(widget)
443+
444+### UTILITIES ###
445+def loadurl(url):
446+ try:
447+ in_file = urllib2.urlopen(url, "r")
448+ text = in_file.read()
449+ in_file.close()
450+ return text
451+ except:
452+ return ''
453+

Subscribers

People subscribed via source and target branches

to status/vote changes: