Merge lp://staging/~bryce/gtg/import_json into lp://staging/~gtg/gtg/old-trunk
- import_json
- Merge into old-trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Luca Invernizzi (community) | Needs Resubmitting | ||
Bertrand Rousseau (community) | Approve | ||
Review via email: mp+18823@code.staging.launchpad.net |
Commit message
Description of the change
Bryce Harrington (bryce) wrote : | # |
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?
Bryce Harrington (bryce) wrote : | # |
No problem. Example json files can be found at:
http://
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:/
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.
Luca Invernizzi (invernizzi) wrote : | # |
Agreed (We should really have a guide for gtg).
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.
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.
Preview Diff
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">●</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 | + |
This adds a plugin for importing tasks from json formatted files.