gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #00146
[Merge] lp:~gtg-user/gtg/rtm-sync-plugin into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/rtm-sync-plugin into lp:gtg.
Requested reviews:
Gtg developers (gtg)
--
https://code.launchpad.net/~gtg-user/gtg/rtm-sync-plugin/+merge/11633
Your team Gtg developers is subscribed to branch lp:gtg.
=== modified file 'GTG/core/task.py'
--- GTG/core/task.py 2009-08-05 13:48:38 +0000
+++ GTG/core/task.py 2009-09-09 23:10:48 +0000
@@ -19,9 +19,11 @@
from datetime import date
import xml.dom.minidom
+import uuid
from GTG import _
from GTG.tools.dates import strtodate
+from datetime import datetime
class Task:
@@ -37,6 +39,7 @@
#the id of this task in the project should be set
#tid is a string ! (we have to choose a type and stick to it)
self.tid = str(ze_id)
+ self.set_uuid(uuid.uuid4())
self.content = ""
#self.content = \
# "<content>Press Escape or close this task to save it</content>"
@@ -59,6 +62,7 @@
if self.loaded:
self.req._task_loaded(self.tid)
self.attributes={}
+ self._modified_update()
def is_loaded(self):
return self.loaded
@@ -78,6 +82,18 @@
def get_id(self):
return str(self.tid)
+ def set_uuid(self, value ):
+ self.uuid = str(value)
+
+ def get_uuid(self):
+ #NOTE: Transitional if switch, needed to add
+ # the uuid field to tasks created before
+ # adding this field to the task description.
+ if self.uuid == "":
+ self.set_uuid(uuid.uuid4())
+ self.sync()
+ return self.uuid
+
def get_title(self):
return self.title
@@ -145,6 +161,12 @@
workable = False
return workable
+ def get_modified(self):
+ return self.modified
+
+ def set_modified(self, string):
+ self.modified = string
+
def set_due_date(self, fulldate, fromparent=False):
# if fromparent, we set only a date if duedate is not set
#Or if duedate is after the newly set date !
@@ -473,10 +495,15 @@
self.sync()
def sync(self):
+ self._modified_update()
if self.sync_func and self.is_loaded():
self.sync_func(self)
self.req._task_modified(self.tid)
+ def _modified_update(self):
+ self.modified = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
+
+
### TAG FUNCTIONS ############################################################
#
=== added file 'GTG/plugins/rtm-sync.gtg-plugin'
--- GTG/plugins/rtm-sync.gtg-plugin 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm-sync.gtg-plugin 2009-09-10 21:44:23 +0000
@@ -0,0 +1,8 @@
+[GTG Plugin]
+Module=rtm_sync
+Name=GTG-Remember The Milk synchronization
+Description=Plugin for synchronising Getting Things Gnome! with the web service Remember the milk ( http://www.rememberthemilk.com ).\n\n\n Legal note: This product uses the Remember The Milk API but is not endorsed or certified by Remember The Milk.
+Authors=Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+Version=0.1.1
+Dependencies=python-xml,python-simplejson
+Enabled=False
=== added directory 'GTG/plugins/rtm_sync'
=== added file 'GTG/plugins/rtm_sync/__init__.py'
--- GTG/plugins/rtm_sync/__init__.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/__init__.py 2009-09-10 21:37:03 +0000
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import os
+sys.path.insert(0, os.getcwd())
+
+#pyflakes gives a warning on the following line,
+# but it's needed for the plugin to work
+from rtm_sync import RtmSync
=== added file 'GTG/plugins/rtm_sync/generic_proxy.py'
--- GTG/plugins/rtm_sync/generic_proxy.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/generic_proxy.py 2009-09-10 21:37:03 +0000
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+
+class GenericProxy(object):
+
+ def __init__(self):
+ super(GenericProxy, self).__init__()
+ self.task_list = []
+
+ def generateTaskList(self):
+ raise Exception()
=== added file 'GTG/plugins/rtm_sync/generic_task.py'
--- GTG/plugins/rtm_sync/generic_task.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/generic_task.py 2009-09-10 23:17:58 +0000
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+import sys
+import os
+import xml.dom.minidom
+#import xml.utils.iso8601
+#import datetime
+#import time
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))+'/pyrtm')
+#import rtm
+from utility import iso8601toTime, timeToIso8601, dateToIso8601, timezone
+
+
+class GenericTask(object):
+ """GenericTask is the abstract interface that represents a generic task.
+ GtgTask and RtmTask are the implementation of this"""
+
+ title = property(lambda self: self._get_title(),
+ lambda self, arg: self._set_title(arg))
+
+ id = property(lambda self: self._get_id())
+ #NOTE: text is the task extended description (or notes
+ # in rtm)
+ text = property(lambda self: self._get_text(),
+ lambda self, arg: self._set_text(arg))
+
+ modified = property(lambda self: self._get_modified())
+
+ due_date = property(lambda self: self._get_due_date(),
+ lambda self, arg: self._set_due_date(arg))
+
+ tags = property(lambda self: self._get_tags(),
+ lambda self, arg: self._set_tags(arg))
+
+ def __str__(self):
+ return "Task " + self.title + "(" + self.id + ")"
+
+ def copy(self, task):
+ self.title = task.title
+ self.tags = task.tags
+ self.text = task.text
+ self.due_date = task.due_date
+
+ #Interface specification that will be overwritten
+ # by the derived classes
+ def delete(self):
+ raise Exception()
+
+
+class RtmTask(GenericTask):
+
+ def __init__(self, task, list_id, taskseries_id, rtm, timeline):
+ super(RtmTask, self).__init__()
+ self.rtm = rtm
+ self.timeline = timeline
+ self.task = task
+ self.list_id = list_id
+ self.taskseries_id = taskseries_id
+
+ def _get_title(self):
+ return self.task.name
+
+ def _set_title(self, title):
+ self.rtm.tasks.setName(timeline=self.timeline, \
+ list_id =self.list_id, \
+ taskseries_id=self.taskseries_id, \
+ task_id=self.id, \
+ name = title)
+
+ def _get_id(self):
+ if hasattr(self.task, 'task'):
+ return self.task.task.id
+ else:
+ return self.task.id
+
+ def _get_tags(self):
+ if hasattr(self.task.tags, 'tag'):
+ if type(self.task.tags.tag) ==list:
+ return self.task.tags.tag
+ else:
+ return [self.task.tags.tag]
+ elif hasattr(self.task.tags, 'list'):
+ return map(lambda x: x.tag if hasattr(x, 'tag') else None, \
+ self.task.tags.list)
+ return []
+
+ def _set_tags(self, tags):
+ tagstxt=""
+ for tag in tags:
+ name = tag.get_name()
+ name_fixed = name[name.find('@')+1:]
+ if tagstxt == "":
+ tagstxt = name_fixed
+ else:
+ tagstxt = tagstxt+ ", " + name_fixed
+ self.rtm.tasks.setTags(timeline=self.timeline, \
+ list_id =self.list_id, \
+ taskseries_id=self.taskseries_id, \
+ task_id=self.id, \
+ tags=tagstxt)
+
+ def _get_text(self):
+ if hasattr(self.task, 'notes') and \
+ hasattr(self.task.notes, 'note'):
+ #Rtm saves the notes text inside the member "$t". Don't ask me why.
+ if type(self.task.notes.note) == list:
+ return "".join(map(lambda note: getattr(note, '$t') + "\n", \
+ self.task.notes.note))
+ else:
+ return getattr(self.task.notes.note, '$t')
+ else:
+ return ""
+
+ def _set_text(self, text):
+ #delete old notes
+ #FIXME: the first check *should* not be necessary (but it is?).
+ if hasattr(self.task, 'notes') and \
+ hasattr(self.task.notes, 'note'):
+ if type(self.task.notes.note) == list:
+ note_ids =map(lambda note: note.id, self.task.notes.note)
+ else:
+ note_ids = [self.task.notes.note.id]
+ map(lambda id: self.rtm.tasksNotes.delete(timeline=self.timeline, \
+ note_id=id), note_ids)
+ #add a new one
+ #TODO: investigate what is "Note title", since there doesn't seem to
+ #be a note
+ # title in the web access.
+ #FIXME: minidom this way is ok, or do we suppose to get multiple
+ # nodes in "content"?
+ if text == "":
+ return
+ document = xml.dom.minidom.parseString(text)
+ content =document.getElementsByTagName("content")
+ if len(content)>0 and hasattr(content[0], 'firstChild') \
+ and hasattr(content[0].firstChild, 'data'):
+ content = content[0].firstChild.data
+ else:
+ return
+ self.rtm.tasksNotes.add(timeline=self.timeline, \
+ list_id = self.list_id,\
+ taskseries_id = self.taskseries_id, \
+ task_id = self.id, \
+ note_title="",\
+ note_text = content)
+
+ def _get_due_date(self):
+ if hasattr(self.task.task, 'due') and self.task.task.due != "":
+ return iso8601toTime(self.task.task.due) - timezone()
+ return None
+
+ def _set_due_date(self, due):
+ if type(due) != type(None):
+ due_string = timeToIso8601(due + timezone())
+ self.rtm.tasks.setDueDate(timeline=self.timeline, \
+ list_id = self.list_id,\
+ taskseries_id = self.taskseries_id, \
+ task_id = self.id, \
+ due=due_string)
+ else:
+ self.rtm.tasks.setDueDate(timeline=self.timeline, \
+ list_id = self.list_id,\
+ taskseries_id = self.taskseries_id, \
+ task_id = self.id)
+
+ def _get_modified(self):
+ if not hasattr(self.task, 'modified') or self.task.modified == "":
+ return None
+
+ return iso8601toTime(self.task.modified) - timezone()
+
+ def delete(self):
+ self.rtm.tasks.delete(timeline = self.timeline, \
+ list_id = self.list_id, \
+ taskseries_id = self.taskseries_id, \
+ task_id = self.id)
+
+
+class GtgTask(GenericTask):
+
+ def __init__(self, task, plugin_api):
+ super(GtgTask, self).__init__()
+ self.task = task
+ self.plugin_api = plugin_api
+
+ def _get_title(self):
+ return self.task.get_title()
+
+ def _set_title(self, title):
+ self.task.set_title(title)
+
+ def _get_id(self):
+ return self.task.get_uuid()
+
+ def _get_tags(self):
+ return self.task.get_tags()
+
+ def _set_tags(self, tags):
+ #NOTE: isn't there a better mode than removing all tags?
+ # need to add function in GTG/core/task.py
+ old_tags = self.tags
+ for tag in old_tags:
+ self.task.remove_tag(tag)
+ map(lambda tag: self.task.add_tag('@'+tag), tags)
+
+ def _get_text(self):
+ return self.task.get_text()
+
+ def _set_text(self, text):
+ self.task.set_text(text)
+
+ def _get_due_date(self):
+ due_string = self.task.get_due_date()
+ if due_string == "":
+ return None
+ return iso8601toTime(due_string)
+
+ def _set_due_date(self, due):
+ due_string = ""
+ if type(due) != None:
+ due_string = dateToIso8601(due)
+ self.task.set_due_date(due_string)
+
+ def _get_modified(self):
+ modified = self.task.get_modified()
+ if modified == None or modified == "":
+ return None
+ return iso8601toTime(modified)
+
+ def delete(self):
+ self.plugin_api.get_requester().delete_task(self.task.get_id())
=== added file 'GTG/plugins/rtm_sync/gtg_proxy.py'
--- GTG/plugins/rtm_sync/gtg_proxy.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/gtg_proxy.py 2009-09-10 21:37:03 +0000
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+import sys
+import os
+# IMPORTANT This add's the plugin's path to python sys path
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+from generic_task import GtgTask
+from generic_proxy import GenericProxy
+
+
+class GtgProxy(GenericProxy):
+
+ def __init__(self, plugin_api):
+ super(GtgProxy, self).__init__()
+ self.plugin_api = plugin_api
+
+ def generateTaskList(self):
+ tasks = map(self.plugin_api.get_task, \
+ self.plugin_api.get_requester().get_active_tasks_list())
+ map(lambda task: self.task_list.append(GtgTask(task, \
+ self.plugin_api)), tasks)
+
+ def newTask(self, title, never_seen_before):
+ new_task = GtgTask(self.plugin_api.get_requester().new_task(
+ newtask=never_seen_before), self.plugin_api)
+ new_task.title = title
+ self.task_list.append(new_task)
+ return new_task
=== added file 'GTG/plugins/rtm_sync/gtk.glade'
--- GTG/plugins/rtm_sync/gtk.glade 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/gtk.glade 2009-09-11 21:43:31 +0000
@@ -0,0 +1,360 @@
+<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
+<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
+
+<glade-interface>
+
+<widget class="GtkDialog" id="dialogtoken">
+ <property name="width_request">500</property>
+ <property name="height_request">150</property>
+ <property name="title" translatable="yes">Authentication</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+ <property name="modal">False</property>
+ <property name="default_width">502</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+ <property name="icon_name">gtk-dialog-info</property>
+ <property name="decorated">True</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">False</property>
+ <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+ <property name="gravity">GDK_GRAVITY_CENTER</property>
+ <property name="focus_on_map">True</property>
+ <property name="urgency_hint">True</property>
+ <property name="has_separator">True</property>
+
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_END</property>
+
+ <child>
+ <widget class="GtkButton" id="btn_ok">
+ <property name="visible">True</property>
+ <property name="can_default">True</property>
+ <property name="can_focus">True</property>
+ <property name="label">gtk-ok</property>
+ <property name="use_stock">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="focus_on_click">True</property>
+ <property name="response_id">-5</property>
+ <signal name="clicked" handler="on_btn_ok_clicked" last_modification_time="Sat, 08 Aug 2009 13:27:39 GMT"/>
+ </widget>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="lbl_title">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><b>Remember the milk</b></property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_CENTER</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">6</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="lbl_dialog">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Please authenticate to Remember The Milk in the browser that is being opened now. When done, press OK
+</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_CENTER</property>
+ <property name="wrap">True</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0</property>
+ <property name="xpad">0</property>
+ <property name="ypad">10</property>
+ <property name="mnemonic_widget">btn_ok</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+<widget class="GtkWindow" id="dialogsync">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes">Synchronization with RTM</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+ <property name="modal">False</property>
+ <property name="resizable">False</property>
+ <property name="destroy_with_parent">False</property>
+ <property name="icon">icons/hicolor/16x16/rtm_image.png</property>
+ <property name="decorated">True</property>
+ <property name="skip_taskbar_hint">False</property>
+ <property name="skip_pager_hint">False</property>
+ <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+ <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
+ <property name="focus_on_map">True</property>
+ <property name="urgency_hint">False</property>
+
+ <child>
+ <widget class="GtkVBox" id="vbox2">
+ <property name="width_request">350</property>
+ <property name="height_request">120</property>
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="pixbuf">icons/hicolor/svg/rtm_image.svg</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="lbl_dialog">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"></property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">True</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkHBox" id="hbox1">
+ <property name="height_request">31</property>
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkProgressBar" id="progressbar">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_PROGRESS_LEFT_TO_RIGHT</property>
+ <property name="fraction">0</property>
+ <property name="pulse_step">0.10000000149</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_END</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkButton" id="btn_ok">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label">gtk-ok</property>
+ <property name="use_stock">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="focus_on_click">True</property>
+ <signal name="clicked" handler="on_btn_ok_clicked" last_modification_time="Sat, 08 Aug 2009 14:30:02 GMT"/>
+ </widget>
+ <packing>
+ <property name="padding">3</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">4</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+<widget class="GtkDialog" id="notification">
+ <property name="width_request">500</property>
+ <property name="height_request">150</property>
+ <property name="title" translatable="yes">Authentication</property>
+ <property name="type">GTK_WINDOW_TOPLEVEL</property>
+ <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+ <property name="modal">False</property>
+ <property name="default_width">502</property>
+ <property name="resizable">True</property>
+ <property name="destroy_with_parent">False</property>
+ <property name="icon_name">gtk-dialog-authentication</property>
+ <property name="decorated">True</property>
+ <property name="skip_taskbar_hint">True</property>
+ <property name="skip_pager_hint">False</property>
+ <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+ <property name="gravity">GDK_GRAVITY_CENTER</property>
+ <property name="focus_on_map">True</property>
+ <property name="urgency_hint">True</property>
+ <property name="has_separator">True</property>
+
+ <child internal-child="vbox">
+ <widget class="GtkVBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">0</property>
+
+ <child internal-child="action_area">
+ <widget class="GtkHButtonBox" id="dialog-action_area1">
+ <property name="visible">True</property>
+ <property name="layout_style">GTK_BUTTONBOX_END</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkVBox" id="vbox1">
+ <property name="visible">True</property>
+ <property name="homogeneous">True</property>
+ <property name="spacing">0</property>
+
+ <child>
+ <widget class="GtkLabel" id="lbl_title">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes"><b>Remember the milk</b></property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_CENTER</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">6</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <widget class="GtkLabel" id="lbl_dialog">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_CENTER</property>
+ <property name="wrap">True</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0</property>
+ <property name="xpad">0</property>
+ <property name="ypad">10</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+ </widget>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </widget>
+ </child>
+</widget>
+
+</glade-interface>
=== added directory 'GTG/plugins/rtm_sync/icons'
=== added directory 'GTG/plugins/rtm_sync/icons/hicolor'
=== added directory 'GTG/plugins/rtm_sync/icons/hicolor/16x16'
=== added file 'GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png'
Binary files GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png 1970-01-01 00:00:00 +0000 and GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png 2009-09-11 20:47:31 +0000 differ
=== added directory 'GTG/plugins/rtm_sync/icons/hicolor/24x24'
=== added file 'GTG/plugins/rtm_sync/icons/hicolor/24x24/rtm_image.png'
Binary files GTG/plugins/rtm_sync/icons/hicolor/24x24/rtm_image.png 1970-01-01 00:00:00 +0000 and GTG/plugins/rtm_sync/icons/hicolor/24x24/rtm_image.png 2009-09-11 20:47:31 +0000 differ
=== added directory 'GTG/plugins/rtm_sync/icons/hicolor/svg'
=== added file 'GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg'
--- GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg 2009-09-11 20:47:31 +0000
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64px"
+ height="64px"
+ id="svg3727"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="rtm_image.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/home/luca/gtg/rtm-sync-plugin/GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png"
+ inkscape:export-xdpi="22.5"
+ inkscape:export-ydpi="22.5">
+ <defs
+ id="defs3729">
+ <filter
+ inkscape:collect="always"
+ id="filter3502"
+ x="-0.044082869"
+ width="1.0881657"
+ y="-0.19633912"
+ height="1.3926782">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="1.4852688"
+ id="feGaussianBlur3504" />
+ </filter>
+ <linearGradient
+ id="linearGradient3661">
+ <stop
+ style="stop-color:#3399ff;stop-opacity:1;"
+ offset="0"
+ id="stop3663" />
+ <stop
+ id="stop3675"
+ offset="0.5"
+ style="stop-color:#3399ff;stop-opacity:1;" />
+ <stop
+ style="stop-color:#3399ff;stop-opacity:0.68627451;"
+ offset="0.75"
+ id="stop3685" />
+ <stop
+ id="stop3687"
+ offset="0.875"
+ style="stop-color:#3399ff;stop-opacity:0.52941176;" />
+ <stop
+ style="stop-color:#3399ff;stop-opacity:0.37719297;"
+ offset="1"
+ id="stop3665" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3661"
+ id="linearGradient3725"
+ gradientUnits="userSpaceOnUse"
+ x1="129.75728"
+ y1="658.44305"
+ x2="232.12813"
+ y2="657.89764" />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath3545">
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="rect3547"
+ width="188.70778"
+ height="271.60831"
+ x="83.991325"
+ y="571.32098" />
+ </clipPath>
+ <linearGradient
+ id="linearGradient3677">
+ <stop
+ style="stop-color:#ececec;stop-opacity:1"
+ offset="0"
+ id="stop3679" />
+ <stop
+ id="stop3689"
+ offset="0.5"
+ style="stop-color:#ffffff;stop-opacity:0.49803922;" />
+ <stop
+ style="stop-color:#ececec;stop-opacity:1"
+ offset="1"
+ id="stop3681" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3677"
+ id="linearGradient3723"
+ gradientUnits="userSpaceOnUse"
+ x1="115.46449"
+ y1="774.37683"
+ x2="235.97925"
+ y2="775.91943" />
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 32 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="64 : 32 : 1"
+ inkscape:persp3d-origin="32 : 21.333333 : 1"
+ id="perspective3735" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.9445436"
+ inkscape:cx="21.790111"
+ inkscape:cy="76.61186"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:document-units="px"
+ inkscape:grid-bbox="true"
+ inkscape:window-width="640"
+ inkscape:window-height="628"
+ inkscape:window-x="506"
+ inkscape:window-y="22" />
+ <metadata
+ id="metadata3732">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ id="g3711"
+ transform="matrix(0.1214085,0.1214085,-0.1214085,0.1214085,61.002827,-86.966337)"
+ inkscape:transform-center-x="-16.513266"
+ inkscape:transform-center-y="-36.581756">
+ <path
+ id="path3396"
+ d="M 414.51135,403.70408 C 414.51135,414.20049 395.48743,422.71931 372.04725,422.71931 C 348.60706,422.71931 329.58314,414.20049 329.58314,403.70408 C 329.58314,393.20767 348.60706,384.68885 372.04725,384.68885 C 395.48743,384.68885 414.51135,393.20767 414.51135,403.70408 z"
+ inkscape:transform-center-y="41.974325"
+ inkscape:transform-center-x="-9.0994502"
+ style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ id="path3400"
+ d="M 414.51135,393.70408 C 414.51135,404.20049 395.48743,412.71931 372.04725,412.71931 C 348.60706,412.71931 329.58314,404.20049 329.58314,393.70408 C 329.58314,383.20767 348.60706,374.68885 372.04725,374.68885 C 395.48743,374.68885 414.51135,383.20767 414.51135,393.70408 z"
+ inkscape:transform-center-y="41.974325"
+ inkscape:transform-center-x="-9.0994502"
+ style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ id="path3402"
+ d="M 414.51135,403.70408 C 414.51135,414.20049 395.48743,422.71931 372.04725,422.71931 C 348.60706,422.71931 329.58314,414.20049 329.58314,403.70408 C 329.58314,393.20767 348.60706,384.68885 372.04725,384.68885 C 395.48743,384.68885 414.51135,393.20767 414.51135,403.70408 z"
+ inkscape:transform-center-y="41.974325"
+ inkscape:transform-center-x="-9.0994502"
+ style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ transform="matrix(0.9856192,0,0,1,199.34766,-2.7713095)"
+ clip-path="url(#clipPath3545)"
+ sodipodi:nodetypes="cccccccccccccccccccccc"
+ id="path3534"
+ d="M 138.90625,409.375 L 139,410.375 L 139.09375,411.34375 C 140.75679,432.38408 139.68245,453.56341 139.21875,474.59375 C 130.4306,497.46685 103.76148,508.08521 95.34375,531.25 C 94.719742,559.63205 95.8781,588.24637 95.84375,616.71875 C 96.525929,671.97803 96.53609,727.46035 97.21875,782.59375 C 100.28884,803.27664 119.73439,816.6627 138.25,823.09375 C 170.29613,833.76601 208.57233,831.13483 236.25,810.71875 C 247.88637,802.02197 256.05515,787.89092 254.28125,773.0625 C 254.46462,692.74557 255.55912,612.36257 256.0625,532.09375 C 249.30834,511.89397 228.57494,501.13943 217,484.1875 C 206.62003,465.60923 211.70898,431.09823 211.625,409.375 C 187.38542,409.37499 163.14583,409.375 138.90625,409.375 z M 173.96875,528.46875 C 180.68858,528.33398 187.4842,528.79077 194.09375,529.625 C 215.45628,532.8583 239.25025,540.68239 251.28125,559.875 C 259.12082,572.0293 254.62503,588.43585 244.15625,597.4375 C 224.14426,615.68821 195.2559,620.79378 168.875,619.71875 C 144.89679,618.14655 118.37989,611.34257 102.8125,591.625 C 93.866467,580.60806 95.372797,563.83495 105.125,553.8125 C 122.5306,535.22564 149.33071,529.07853 173.96875,528.46875 z"
+ style="fill:url(#linearGradient3723);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ id="path3508"
+ d="M 369.94777,528.00618 C 345.30973,528.61593 318.50962,534.76307 301.10402,553.34993 C 291.35182,563.37235 289.84549,580.14549 298.79152,591.16243 C 314.35891,610.87997 340.87468,617.66905 364.85289,619.24125 C 391.23379,620.31629 420.12328,615.22564 440.13527,596.97493 C 450.60405,587.97325 455.09984,571.56673 447.26027,559.41243 C 435.22927,540.21979 411.4353,532.39573 390.07277,529.16243 C 383.46322,528.32817 376.6676,527.87141 369.94777,528.00618 z"
+ style="opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ style="fill:url(#linearGradient3725);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 138.90625,409.375 L 139,410.375 L 139.09375,411.34375 C 140.75679,432.38408 139.68245,453.56341 139.21875,474.59375 C 130.4306,497.46685 103.76148,508.08521 95.34375,531.25 C 94.719742,559.63205 95.8781,588.24637 95.84375,616.71875 C 96.525929,671.97803 96.53609,631.46035 97.21875,686.59375 C 100.28884,707.27664 119.73439,720.6627 138.25,727.09375 C 170.29613,737.76601 208.57233,735.13483 236.25,714.71875 C 247.88637,706.02197 256.05515,691.89092 254.28125,677.0625 C 254.46462,596.74557 255.55912,612.36257 256.0625,532.09375 C 249.30834,511.89397 228.57494,501.13943 217,484.1875 C 206.62003,465.60923 211.70898,431.09823 211.625,409.375 C 187.38542,409.37499 163.14583,409.375 138.90625,409.375 z M 173.96875,528.46875 C 180.68858,528.33398 187.4842,528.79077 194.09375,529.625 C 215.45628,532.8583 239.25025,540.68239 251.28125,559.875 C 259.12082,572.0293 254.62503,588.43585 244.15625,597.4375 C 224.14426,615.68821 195.2559,620.79378 168.875,619.71875 C 144.89679,618.14655 118.37989,611.34257 102.8125,591.625 C 93.866467,580.60806 95.372797,563.83495 105.125,553.8125 C 122.5306,535.22564 149.33071,529.07853 173.96875,528.46875 z"
+ id="path3659"
+ sodipodi:nodetypes="cccccccccccccccccccccc"
+ clip-path="url(#clipPath3545)"
+ transform="matrix(0.9856192,0,0,1,199.34766,37.228691)" />
+ <path
+ id="path3563"
+ d="M 333.14099,404.25022 C 335.49334,426.41602 333.99301,448.60995 333.79724,470.81275 C 333.33145,472.18625 332.79969,473.46195 332.20349,474.62525 C 322.27441,493.99985 294.30805,508.61965 289.39099,529.43775 C 291.10411,668.49535 291.42224,773.93775 291.42224,773.93775 C 291.40471,774.39025 291.42224,774.85755 291.42224,775.31275 C 291.42226,775.76805 291.40469,776.23525 291.42224,776.68775 L 291.42224,780.46905 L 291.79724,780.46905 C 295.756,807.17195 330.16049,828.06275 372.01599,828.09405 C 372.04741,828.09405 372.07833,828.09405 372.10974,828.09405 C 413.96526,828.06275 448.36974,807.17185 452.32849,780.46905 L 452.70349,780.46905 L 452.70349,776.68775 C 452.72106,776.23515 452.70348,775.76805 452.70349,775.31275 C 452.70351,774.85755 452.72103,774.39025 452.70349,773.93775 C 452.70351,773.93775 452.99041,668.49525 454.70349,529.43775 C 449.78645,508.61975 421.82008,493.99985 411.89099,474.62525 C 411.30699,473.48575 410.78728,472.21675 410.32849,470.87525 L 410.04724,404.25022 L 333.14099,404.25022 z M 337.54724,408.25022 C 360.39099,408.25022 383.23474,408.25022 406.07849,408.25022 C 406.12735,420.43772 406.18585,432.62522 406.23474,444.81275 C 407.10693,454.22905 404.81823,463.84295 406.98474,473.06275 C 415.68479,495.66375 441.89079,505.80335 450.29724,528.71905 C 450.30128,608.04355 448.768,687.54235 448.70349,766.93775 C 450.40115,779.71305 445.88556,792.90285 436.45349,801.71905 C 411.40508,824.19635 374.0633,828.57625 342.29724,820.15645 C 322.49257,814.98065 300.65448,802.54255 295.95349,780.96905 C 294.88669,751.98605 295.34734,722.85275 294.92224,693.81275 C 294.94021,638.71625 293.4657,583.62415 293.79724,528.53145 C 302.5795,505.57595 328.81514,495.25805 337.51599,472.21905 C 338.26259,451.56455 339.063,430.72372 337.67224,410.03142 L 337.60974,409.25022 L 337.54724,408.25022 z"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ id="path3417"
+ d="M 414.47141,393.86713 C 414.16923,404.24732 395.25459,412.61713 372.00266,412.61713 C 349.29499,412.61714 330.73443,404.62835 329.59641,394.58588 L 329.56516,403.61713 C 329.50442,414.11336 348.56248,422.61714 372.00266,422.61713 C 395.44284,422.61713 414.47141,414.11354 414.47141,403.61713 C 414.10399,400.99347 414.71184,396.16626 414.47141,393.86713 z"
+ style="opacity:1;fill:#0060be;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ id="path3404"
+ d="M 414.51135,393.70408 C 414.51135,404.20049 395.48743,412.71931 372.04725,412.71931 C 348.60706,412.71931 329.58314,404.20049 329.58314,393.70408 C 329.58314,383.20767 348.60706,374.68885 372.04725,374.68885 C 395.48743,374.68885 414.51135,383.20767 414.51135,393.70408 z"
+ inkscape:transform-center-y="41.974325"
+ inkscape:transform-center-x="-9.0994502"
+ style="opacity:0.91085271;fill:#0060be;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ sodipodi:nodetypes="cccccccc"
+ id="path3434"
+ d="M 331.61606,403.625 C 332.77186,412.45706 343.31251,415.58934 350.58481,417.96875 C 369.00699,422.23097 390.1947,422.18632 406.67856,412.1875 C 409.76447,410.08284 413.03827,406.75523 412.39731,402.65625 C 399.53954,413.82702 381.02227,415.1994 364.61901,414.32955 C 352.97865,413.20127 340.32229,410.83165 331.61606,402.5 L 331.61606,403.5 L 331.61606,403.625 z"
+ style="opacity:1;fill:#004185;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3502)" />
+ </g>
+ </g>
+</svg>
=== added directory 'GTG/plugins/rtm_sync/pyrtm'
=== added file 'GTG/plugins/rtm_sync/pyrtm/README'
--- GTG/plugins/rtm_sync/pyrtm/README 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/pyrtm/README 2009-08-07 04:13:36 +0000
@@ -0,0 +1,10 @@
+======================================================================
+Python library for Remember The Milk API
+======================================================================
+
+Copyright (c) 2008 by Sridhar Ratnakumar <http://nearfar.org/>
+
+Contributors:
+ - Mariano Draghi (cHagHi) <mariano at chaghi dot com dot ar>
+
+See app.py for examples
=== added file 'GTG/plugins/rtm_sync/pyrtm/rtm.py'
--- GTG/plugins/rtm_sync/pyrtm/rtm.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/pyrtm/rtm.py 2009-08-07 04:13:36 +0000
@@ -0,0 +1,395 @@
+# Python library for Remember The Milk API
+
+__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
+__all__ = (
+ 'API',
+ 'createRTM',
+ 'set_log_level'
+ )
+
+
+#import new
+import warnings
+import urllib
+import logging
+from hashlib import md5
+
+warnings.simplefilter('default', ImportWarning)
+
+_use_simplejson = False
+try:
+ import simplejson
+ _use_simplejson = True
+except ImportError:
+ pass
+
+if not _use_simplejson:
+ warnings.warn("simplejson module is not available, "
+ "falling back to the internal JSON parser. "
+ "Please consider installing the simplejson module from "
+ "http://pypi.python.org/pypi/simplejson.", ImportWarning,
+ stacklevel=2)
+
+logging.basicConfig()
+LOG = logging.getLogger(__name__)
+LOG.setLevel(logging.INFO)
+
+SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
+AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
+
+
+class RTMError(Exception): pass
+
+class RTMAPIError(RTMError): pass
+
+class AuthStateMachine(object):
+
+ class NoData(RTMError): pass
+
+ def __init__(self, states):
+ self.states = states
+ self.data = {}
+
+ def dataReceived(self, state, datum):
+ if state not in self.states:
+ raise RTMError, "Invalid state <%s>" % state
+ self.data[state] = datum
+
+ def get(self, state):
+ if state in self.data:
+ return self.data[state]
+ else:
+ raise AuthStateMachine.NoData, 'No data for <%s>' % state
+
+
+class RTM(object):
+
+ def __init__(self, apiKey, secret, token=None):
+ self.apiKey = apiKey
+ self.secret = secret
+ self.authInfo = AuthStateMachine(['frob', 'token'])
+
+ # this enables one to do 'rtm.tasks.getList()', for example
+ for prefix, methods in API.items():
+ setattr(self, prefix,
+ RTMAPICategory(self, prefix, methods))
+
+ if token:
+ self.authInfo.dataReceived('token', token)
+
+ def _sign(self, params):
+ "Sign the parameters with MD5 hash"
+ pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
+ return md5(self.secret+pairs).hexdigest()
+
+ def get(self, **params):
+ "Get the XML response for the passed `params`."
+ params['api_key'] = self.apiKey
+ params['format'] = 'json'
+ params['api_sig'] = self._sign(params)
+
+ json = openURL(SERVICE_URL, params).read()
+
+ LOG.debug("JSON response: \n%s" % json)
+
+ if _use_simplejson:
+ data = dottedDict('ROOT', simplejson.loads(json))
+ else:
+ data = dottedJSON(json)
+ rsp = data.rsp
+
+ if rsp.stat == 'fail':
+ raise RTMAPIError, 'API call failed - %s (%s)' % (
+ rsp.err.msg, rsp.err.code)
+ else:
+ return rsp
+
+ def getNewFrob(self):
+ rsp = self.get(method='rtm.auth.getFrob')
+ self.authInfo.dataReceived('frob', rsp.frob)
+ return rsp.frob
+
+ def getAuthURL(self):
+ try:
+ frob = self.authInfo.get('frob')
+ except AuthStateMachine.NoData:
+ frob = self.getNewFrob()
+
+ params = {
+ 'api_key': self.apiKey,
+ 'perms' : 'delete',
+ 'frob' : frob
+ }
+ params['api_sig'] = self._sign(params)
+ return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
+
+ def getToken(self):
+ frob = self.authInfo.get('frob')
+ rsp = self.get(method='rtm.auth.getToken', frob=frob)
+ self.authInfo.dataReceived('token', rsp.auth.token)
+ return rsp.auth.token
+
+class RTMAPICategory:
+ "See the `API` structure and `RTM.__init__`"
+
+ def __init__(self, rtm, prefix, methods):
+ self.rtm = rtm
+ self.prefix = prefix
+ self.methods = methods
+
+ def __getattr__(self, attr):
+ if attr in self.methods:
+ rargs, oargs = self.methods[attr]
+ if self.prefix == 'tasksNotes':
+ aname = 'rtm.tasks.notes.%s' % attr
+ else:
+ aname = 'rtm.%s.%s' % (self.prefix, attr)
+ return lambda **params: self.callMethod(
+ aname, rargs, oargs, **params)
+ else:
+ raise AttributeError, 'No such attribute: %s' % attr
+
+ def callMethod(self, aname, rargs, oargs, **params):
+ # Sanity checks
+ for requiredArg in rargs:
+ if requiredArg not in params:
+ raise TypeError, 'Required parameter (%s) missing' % requiredArg
+
+ for param in params:
+ if param not in rargs + oargs:
+ warnings.warn('Invalid parameter (%s)' % param)
+
+ return self.rtm.get(method=aname,
+ auth_token=self.rtm.authInfo.get('token'),
+ **params)
+
+
+
+# Utility functions
+
+def sortedItems(dictionary):
+ "Return a list of (key, value) sorted based on keys"
+ keys = dictionary.keys()
+ keys.sort()
+ for key in keys:
+ yield key, dictionary[key]
+
+def openURL(url, queryArgs=None):
+ if queryArgs:
+ url = url + '?' + urllib.urlencode(queryArgs)
+ LOG.debug("URL> %s", url)
+ return urllib.urlopen(url)
+
+class dottedDict(object):
+ "Make dictionary items accessible via the object-dot notation."
+
+ def __init__(self, name, dictionary):
+ self._name = name
+
+ if type(dictionary) is dict:
+ for key, value in dictionary.items():
+ if type(value) is dict:
+ value = dottedDict(key, value)
+ elif type(value) in (list, tuple) and key != 'tag':
+ value = [dottedDict('%s_%d' % (key, i), item)
+ for i, item in indexed(value)]
+ setattr(self, key, value)
+
+ def __repr__(self):
+ children = [c for c in dir(self) if not c.startswith('_')]
+ return 'dotted <%s> : %s' % (
+ self._name,
+ ', '.join(children))
+
+
+def safeEval(string):
+ return eval(string, {}, {})
+
+def dottedJSON(json):
+ return dottedDict('ROOT', safeEval(json))
+
+def indexed(seq):
+ index = 0
+ for item in seq:
+ yield index, item
+ index += 1
+
+
+# API spec
+
+API = {
+ 'auth': {
+ 'checkToken':
+ [('auth_token'), ()],
+ 'getFrob':
+ [(), ()],
+ 'getToken':
+ [('frob'), ()]
+ },
+ 'contacts': {
+ 'add':
+ [('timeline', 'contact'), ()],
+ 'delete':
+ [('timeline', 'contact_id'), ()],
+ 'getList':
+ [(), ()]
+ },
+ 'groups': {
+ 'add':
+ [('timeline', 'group'), ()],
+ 'addContact':
+ [('timeline', 'group_id', 'contact_id'), ()],
+ 'delete':
+ [('timeline', 'group_id'), ()],
+ 'getList':
+ [(), ()],
+ 'removeContact':
+ [('timeline', 'group_id', 'contact_id'), ()],
+ },
+ 'lists': {
+ 'add':
+ [('timeline', 'name',), ('filter')],
+ 'archive':
+ [('timeline', 'list_id'),()],
+ 'delete':
+ [('timeline', 'list_id'),()],
+ 'getList':
+ [(),()],
+ 'setDefaultList':
+ [('timeline'), ('list_id')],
+ 'setName':
+ [('timeline', 'list_id', 'name')],
+ 'unarchive':
+ [('timeline'), ('list_id')]
+ },
+ 'locations': {
+ 'getList':
+ [(), ()]
+ },
+ 'reflection': {
+ 'getMethodInfo':
+ [('methodName',), ()],
+ 'getMethods':
+ [(), ()]
+ },
+ 'settings': {
+ 'getList':
+ [(), ()]
+ },
+ 'tasks': {
+ 'add':
+ [('timeline', 'name',), ('list_id', 'parse',)],
+ 'addTags':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+ ()],
+ 'complete':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
+ 'delete':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
+ 'getList':
+ [(),
+ ('list_id', 'filter', 'last_sync')],
+ 'movePriority':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
+ ()],
+ 'moveTo':
+ [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
+ ()],
+ 'postpone':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ()],
+ 'removeTags':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+ ()],
+ 'setDueDate':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('due', 'has_due_time', 'parse')],
+ 'setEstimate':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('estimate',)],
+ 'setLocation':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('location_id',)],
+ 'setName':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
+ ()],
+ 'setPriority':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('priority',)],
+ 'setRecurrence':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('repeat',)],
+ 'setTags':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('tags',)],
+ 'setURL':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ('url',)],
+ 'uncomplete':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+ ()],
+ },
+ 'tasksNotes': {
+ 'add':
+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
+ 'delete':
+ [('timeline', 'note_id'), ()],
+ 'edit':
+ [('timeline', 'note_id', 'note_title', 'note_text'), ()]
+ },
+ 'test': {
+ 'echo':
+ [(), ()],
+ 'login':
+ [(), ()]
+ },
+ 'time': {
+ 'convert':
+ [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
+ 'parse':
+ [('text',), ('timezone', 'dateformat')]
+ },
+ 'timelines': {
+ 'create':
+ [(), ()]
+ },
+ 'timezones': {
+ 'getList':
+ [(), ()]
+ },
+ 'transactions': {
+ 'undo':
+ [('timeline', 'transaction_id'), ()]
+ },
+ }
+
+def createRTM(apiKey, secret, token=None):
+ rtm = RTM(apiKey, secret, token)
+# if token is None:
+# print 'No token found'
+# print 'Give me access here:', rtm.getAuthURL()
+# raw_input('Press enter once you gave access')
+# print 'Note down this token for future use:', rtm.getToken()
+
+ return rtm
+
+def test(apiKey, secret, token=None):
+ rtm = createRTM(apiKey, secret, token)
+
+ rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
+ print [t.name for t in rspTasks.tasks.list.taskseries]
+ print rspTasks.tasks.list.id
+
+ rspLists = rtm.lists.getList()
+ # print rspLists.lists.list
+ print [(x.name, x.id) for x in rspLists.lists.list]
+
+def set_log_level(level):
+ '''Sets the log level of the logger used by the module.
+
+ >>> import rtm
+ >>> import logging
+ >>> rtm.set_log_level(logging.INFO)
+ '''
+
+ LOG.setLevel(level)
=== added file 'GTG/plugins/rtm_sync/rtm_proxy.py'
--- GTG/plugins/rtm_sync/rtm_proxy.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/rtm_proxy.py 2009-09-10 21:37:03 +0000
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+import os
+import sys
+#import time
+import subprocess
+#import gobject
+from xdg.BaseDirectory import xdg_config_home
+#import xml.utils.iso8601
+#from datetime import date
+
+#This add's the plugin's path to python sys path
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))+'/pyrtm')
+import rtm
+import utility
+from generic_task import RtmTask
+from generic_proxy import GenericProxy
+
+
+class RtmProxy(GenericProxy):
+
+ def __init__(self):
+ super(RtmProxy, self).__init__()
+ self.token = None
+
+ def getToken(self):
+ """gets a token from file (if a previous sync has been
+ performed), or opens a browser to request a new one
+ (in which case the function returns true). NOTE: token
+ is valid forever """
+ if self.token == None:
+ self.config_dir = \
+ os.path.join(xdg_config_home, 'gtg/plugins/rtm-sync')
+ self.token = utility.smartLoadFromFile(self.config_dir, 'token')
+ if self.token == None:
+ self.rtm=rtm.createRTM("2a440fdfe9d890c343c25a91afd84c7e", \
+ "ca078fee48d0bbfa")
+ subprocess.Popen(['xdg-open', self.rtm.getAuthURL()])
+ return False
+ return True
+
+ def login(self):
+ #TODO: handling connection failures and denial of access, proper
+ # interface
+ # assert(self.token != None), "Token must be requested before
+ # calling synchronize"
+ if hasattr(self, 'rtm'):
+ self.token = self.rtm.getToken()
+ if(self.getToken() == False):
+ return False
+ self.rtm=rtm.createRTM("2a440fdfe9d890c343c25a91afd84c7e",\
+ "ca078fee48d0bbfa", self.token)
+ utility.smartSaveToFile(self.config_dir, 'token', self.token)
+ #NOTE: a timeline is an undo list for RTM. It can be used for
+ # journaling(timeline rollback is atomical)
+ self.timeline = self.rtm.timelines.create().timeline
+ return True
+
+ def downloadFromWeb(self):
+ #NOTE: syncing only incomplete tasks for now
+ #(it's easier to debug the things you see)
+ lists_id_list = map(lambda x: x.id, \
+ self.rtm.lists.getList().lists.list)
+
+ def get_list_of_taskseries(x):
+ currentlist = self.rtm.tasks.getList(filter='status:incomplete', \
+ list_id=x).tasks
+ if hasattr(currentlist, 'list'):
+ return currentlist.list
+ else:
+ return []
+ task_list_global= map(get_list_of_taskseries, lists_id_list)
+ taskseries_list = filter(lambda x: hasattr(x[0], 'taskseries'), \
+ zip(task_list_global, lists_id_list))
+ tasks_list_wrapped = map(lambda x: (x[0].taskseries, x[1]), \
+ taskseries_list)
+ tasks_list_normalized = map(lambda x: zip(x[0], [x[1]] * len(x[0]), \
+ map(lambda x: x.id, x[0])) if type(x[0]) == list \
+ else [(x[0], x[1], x[0].id)], tasks_list_wrapped)
+ tasks_list_unwrapped = []
+ task_objects_list = []
+ list_ids_list = []
+ taskseries_ids_list = []
+ if len(tasks_list_normalized)>0:
+ tasks_list_unwrapped = reduce(lambda x, y: x+y, \
+ tasks_list_normalized)
+ task_objects_list, list_ids_list, taskseries_ids_list = \
+ utility.unziplist(tasks_list_unwrapped)
+
+ return zip(task_objects_list, list_ids_list, taskseries_ids_list)
+
+ def generateTaskList(self):
+ data = self.downloadFromWeb()
+ for task, list_id, taskseries_id in data:
+ self.task_list.append(RtmTask(task, list_id, taskseries_id, \
+ self.rtm, self.timeline))
+
+ def newTask(self, title):
+ result = self.rtm.tasks.add(timeline=self.timeline, name=title)
+ new_task= RtmTask(result.list.taskseries.task, result.list.id,\
+ result.list.taskseries.id, self.rtm, self.timeline)
+ self.task_list.append(new_task)
+ return new_task
=== added file 'GTG/plugins/rtm_sync/rtm_sync.py'
--- GTG/plugins/rtm_sync/rtm_sync.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/rtm_sync.py 2009-09-11 16:10:18 +0000
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+# - Paulo Cabido <paulo.cabido@xxxxxxxxx> (example file)
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gtk
+#import pygtk
+import os
+import sys
+from threading import Thread
+#import gobject
+#import logging
+# IMPORTANT This add's the plugin's path to python sys path
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))+'/pyrtm')
+import syncengine
+
+
+class RtmSync:
+ plugin_api = None
+ worker_thread = None
+ sync_engine = None
+ progressbar = None
+ progressbar_percent =0
+ status = None
+ lbl_dialog = None
+
+ def __init__(self):
+ #Icons!
+ self.plugin_path = os.path.dirname(os.path.abspath(__file__))
+ rtm_image_path = os.path.join(self.plugin_path,\
+ "icons/hicolor/16x16/rtm_image.png")
+ pixbug_rtm = gtk.gdk.\
+ pixbuf_new_from_file_at_size(rtm_image_path, 16, 16)
+ rtm_toolbar_image = gtk.Image()
+ rtm_menu_image = gtk.Image()
+ rtm_toolbar_image.set_from_pixbuf(pixbug_rtm)
+ rtm_menu_image.set_from_pixbuf(pixbug_rtm)
+ rtm_toolbar_image.show()
+ rtm_menu_image.show()
+
+ #drop down menu
+ self.menu_item = gtk.ImageMenuItem("Synchronize with RTM")
+ self.menu_item.connect('activate', self.onTesteMenu)
+ self.menu_item.set_image(rtm_menu_image)
+
+ #toolbar button
+ self.tb_button = gtk.ToolButton(rtm_toolbar_image)
+ self.tb_button.set_label("Synchronize RTM")
+ self.tb_button.connect('clicked', self.onTbButton)
+
+ # plugin engine methods
+ def activate(self, plugin_api):
+ self.plugin_api = plugin_api
+ # add a menu item to the menu bar
+ plugin_api.add_menu_item(self.menu_item)
+
+ # saves the separator's index to later remove it
+ self.separator = plugin_api.add_toolbar_item(gtk.SeparatorToolItem())
+ # add a item(button) to the ToolBar
+ plugin_api.add_toolbar_item(self.tb_button)
+
+ def deactivate(self, plugin_api):
+ plugin_api.remove_menu_item(self.menu_item)
+ plugin_api.remove_toolbar_item(self.tb_button)
+ plugin_api.remove_toolbar_item(None, self.separator)
+
+ #load a dialog with a String
+ def loadDialogToken(self, msg):
+ path = os.path.dirname(os.path.abspath(__file__))
+ glade_file = os.path.join(path, "gtk.glade")
+ wTree = gtk.glade.XML(glade_file, "dialogtoken")
+ self.dialog = wTree.get_widget("dialogtoken")
+ self.btn_ok = wTree.get_widget("btn_ok")
+ self.lbl_dialog = wTree.get_widget("lbl_dialog")
+ self.lbl_dialog.set_markup(msg)
+ self.dialog.connect("delete_event", self.close_dialog)
+ self.btn_ok.connect("clicked", self.callback)
+ self.dialog.show_all()
+
+ def loadDialogSync(self, msg):
+ path = os.path.dirname(os.path.abspath(__file__))
+ glade_file = os.path.join(path, "gtk.glade")
+ wTree = gtk.glade.XML(glade_file, "dialogsync")
+ self.dialog = wTree.get_widget("dialogsync")
+ self.btn_ok = wTree.get_widget("btn_ok")
+ self.btn_ok.set_sensitive(False)
+ self.lbl_dialog = wTree.get_widget("lbl_dialog")
+ self.lbl_dialog.set_text(msg)
+ self.progressbar = wTree.get_widget("progressbar")
+ self.dialog.connect("delete_event", self.close_dialog)
+ self.btn_ok.connect("clicked", self.close_dialog)
+ self.dialog.show_all()
+
+ def loadDialogNotification(self, msg):
+ path = os.path.dirname(os.path.abspath(__file__))
+ glade_file = os.path.join(path, "gtk.glade")
+ wTree = gtk.glade.XML(glade_file, "notification")
+ self.dialog = wTree.get_widget("notification")
+ self.lbl_dialog = wTree.get_widget("lbl_dialog")
+ self.lbl_dialog.set_text(msg)
+ self.dialog.show_all()
+
+ def close_dialog(self, widget, data=None):
+ self.dialog.destroy()
+
+ def set_progressbar(self):
+ self.progressbar.set_fraction(self.progressbar_percent)
+ if self.progressbar_percent == 1.0:
+ self.btn_ok.set_sensitive(True)
+
+ def set_status(self):
+ self.lbl_dialog.set_text(self.status)
+
+ def set_substatus(self):
+ self.progressbar.set_text(self.substatus)
+
+ def onTesteMenu(self, widget):
+ self.onTbButton(widget)
+
+ def lauchSynchronization(self):
+ self.loadDialogSync("Synchronization started")
+ self.worker_thread = Thread(target = \
+ self.sync_engine.synchronize).start()
+
+ def onTbButton(self, widget):
+ self.sync_engine=syncengine.SyncEngine(self)
+ self.checkLogin()
+
+ def checkLoginBtn(self, widget):
+ self.dialog.destroy()
+ self.checkLogin(False)
+
+ def checkLogin (self, firstime = True):
+ login = False
+ self.loadDialogNotification("Trying to access, please stand by...")
+ try:
+ login = self.sync_engine.rtmLogin()
+ except:
+ pass
+ self.dialog.destroy()
+ if login == False:
+ if not firstime:
+ self.callback = self.close_dialog
+ self.loadDialogToken("<b>Authentication failed<b>. Please retry.")
+ else:
+ self.callback = self.close_dialog
+ self.callback = self.checkLoginBtn
+ self.loadDialogToken("Please authenticate to Remember \
+The Milk in the browser that is being opened now. \
+When done, press OK")
+ else:
+ self.lauchSynchronization()
+
+ def onTaskOpened(self, plugin_api):
+ pass
=== added file 'GTG/plugins/rtm_sync/syncengine.py'
--- GTG/plugins/rtm_sync/syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/syncengine.py 2009-09-11 15:39:52 +0000
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+from time import sleep
+#import subprocess
+import gobject
+from xdg.BaseDirectory import xdg_cache_home
+#import pickle
+#import xml.utils.iso8601
+#from datetime import date
+
+# IMPORTANT This add's the plugin's path to python sys path
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))+'/pyrtm')
+from gtg_proxy import GtgProxy
+from rtm_proxy import RtmProxy
+from utility import smartSaveToFile, smartLoadFromFile, filterAttr, unziplist
+import rtm
+
+
+class SyncEngine:
+
+ def __init__(self, this_plugin):
+ self.this_plugin = this_plugin
+ self.rtm_proxy = RtmProxy()
+ self.gtg_proxy = GtgProxy(self.this_plugin.plugin_api)
+
+ def rtmLogin(self):
+ return self.rtm_proxy.login()
+
+ def _firstSynchronization(self):
+ gtg_to_rtm_id_mapping = []
+ #generating sets to perform intersection of tasks
+ #NOTE: assuming different titles!
+ gtg_task_titles_set = set(map(lambda x: x.title, self.gtg_list))
+ rtm_task_titles_set = set(map(lambda x: x.title, self.rtm_list))
+ #tasks in common
+ for title in rtm_task_titles_set.intersection(gtg_task_titles_set):
+ gtg_to_rtm_id_mapping.append(
+ (filterAttr(self.gtg_list, 'title', title)[0].id,
+ filterAttr(self.rtm_list, 'title', title)[0].id))
+
+ #tasks that must be added to GTG
+ rtm_added = rtm_task_titles_set.difference(gtg_task_titles_set)
+ if len(rtm_added) > 0:
+ self.update_status("Adding tasks to gtg..")
+ self.update_progressbar(0.4)
+ for title in rtm_added:
+ self.update_substatus("Adding " + title)
+ base_task = filterAttr(self.rtm_list, 'title', title)[0]
+ new_task = self.gtg_proxy.newTask(title, True)
+ new_task.copy(base_task)
+ gtg_to_rtm_id_mapping.append((new_task.id, base_task.id))
+
+ #tasks that must be added to RTM
+ gtg_added = gtg_task_titles_set.difference(rtm_task_titles_set)
+ if len(gtg_added) > 0:
+ self.update_status("Adding tasks to rtm..")
+ self.update_progressbar(0.5)
+ for title in gtg_added:
+ self.update_substatus("Adding " + title)
+ base_task = filterAttr(self.gtg_list, 'title', title)[0]
+ new_task = self.rtm_proxy.newTask(title)
+ new_task.copy(base_task)
+ gtg_to_rtm_id_mapping.append((base_task.id, new_task.id))
+ return gtg_to_rtm_id_mapping
+
+ def synchronize(self):
+ try:
+ self.synchronizeWorker()
+ except rtm.RTMAPIError as exception:
+ self.close_gui(exception.message)
+ except rtm.RTMError as exception:
+ self.close_gui(exception.message)
+ except:
+ self.close_gui("Synchronization failed.")
+
+ def synchronizeWorker(self):
+ self.update_status("Downloading task list...")
+ self.update_progressbar(0.1)
+
+ self.gtg_proxy.generateTaskList()
+ self.rtm_proxy.generateTaskList()
+
+ self.update_status("Analyzing tasks...")
+ self.update_progressbar(0.2)
+ self.gtg_list = self.gtg_proxy.task_list
+ self.rtm_list = self.rtm_proxy.task_list
+
+ ## loading the mapping of the last sync
+ cache_dir = os.path.join(xdg_cache_home, 'gtg/plugins/rtm-sync')
+ gtg_to_rtm_id_mapping = smartLoadFromFile(\
+ cache_dir, 'gtg_to_rtm_id_mapping')
+ if gtg_to_rtm_id_mapping is None:
+ ###this is the first synchronization
+ self.update_status("Running first synchronization...")
+ self.update_progressbar(0.3)
+ gtg_to_rtm_id_mapping = \
+ self._firstSynchronization()
+ else:
+ ###this is an update
+ self.update_status("Analyzing last sync...")
+ self.update_progressbar(0.3)
+ gtg_id_current_set = set(map(lambda x: x.id, self.gtg_list))
+ rtm_id_current_set = set(map(lambda x: x.id, self.rtm_list))
+ if len(gtg_to_rtm_id_mapping)>0:
+ gtg_id_previous_list, rtm_id_previous_list = \
+ unziplist(gtg_to_rtm_id_mapping)
+ else:
+ gtg_id_previous_list, rtm_id_previous_list=[], []
+ gtg_id_previous_set = set(gtg_id_previous_list)
+ rtm_id_previous_set = set(rtm_id_previous_list)
+ gtg_to_rtm_id_dict = dict(gtg_to_rtm_id_mapping)
+ rtm_to_gtg_id_dict = dict(zip(rtm_id_previous_list, \
+ gtg_id_previous_list))
+
+ #We'll generate a new mapping between gtg and rtm task ids
+ gtg_to_rtm_id_mapping = []
+
+ #tasks removed from gtg since last synchronization
+ gtg_removed = gtg_id_previous_set.difference(gtg_id_current_set)
+ #tasks removed from rtm since last synchronization
+ rtm_removed = rtm_id_previous_set.difference(rtm_id_current_set)
+ #tasks added to gtg since last synchronization
+ gtg_added = gtg_id_current_set.difference(gtg_id_previous_set)
+ #tasks added to rtm since last synchronization
+ rtm_added = rtm_id_current_set.difference(rtm_id_previous_set)
+ #tasks still in common(which may need to be updated)
+ gtg_common = gtg_id_current_set.difference(gtg_added)\
+ .difference(gtg_removed)
+
+ #Delete from rtm the tasks that have been removed in gtg
+ if len(gtg_removed) > 0:
+ self.update_status("Deleting tasks from rtm..")
+ self.update_progressbar(0.4)
+ for gtg_id in gtg_removed:
+ rtm_id = gtg_to_rtm_id_dict[gtg_id]
+ rtm_task = filterAttr(self.rtm_list, 'id', rtm_id)
+ self.update_substatus("Deleting " + rtm_task.title)
+ map(lambda task: task.delete(), rtm_task)
+
+ #Delete from gtg the tasks that have been removed in rtm
+ if len(rtm_removed) > 0:
+ self.update_status("Deleting tasks from gtg..")
+ self.update_progressbar(0.5)
+ for rtm_id in rtm_removed:
+ gtg_id = rtm_to_gtg_id_dict[rtm_id]
+ gtg_task = filterAttr(self.gtg_list, 'id', gtg_id)
+ self.update_substatus("Deleting " + gtg_task.title)
+ map(lambda task: task.delete(), gtg_task)
+ gtg_common.discard(gtg_id)
+
+ #tasks that must be added to RTM
+ #NOTE: should we check if the title is already present in the
+ #other backend, to be more robust?(Idem for vice-versa)
+ if len(gtg_added) >0:
+ self.update_status("Adding tasks to rtm..")
+ self.update_progressbar(0.6)
+ for gtg_id in gtg_added:
+ gtg_task = filterAttr(self.gtg_list, 'id', gtg_id)[0]
+ self.update_substatus("Adding " + gtg_task.title)
+ rtm_task = self.rtm_proxy.newTask(gtg_task.title)
+ rtm_task.copy(gtg_task)
+ gtg_to_rtm_id_mapping.append((gtg_id, rtm_task.id))
+
+ #tasks that must be added to GTG
+ if len(rtm_added) >0:
+ self.update_status("Adding tasks to rtm..")
+ self.update_progressbar(0.7)
+ for rtm_id in rtm_added:
+ rtm_task = filterAttr(self.rtm_list, 'id', rtm_id)[0]
+ self.update_substatus("Adding " + rtm_task.title)
+ gtg_task = self.gtg_proxy.newTask(rtm_task.title, True)
+ gtg_task.copy(rtm_task)
+ gtg_to_rtm_id_mapping.append((gtg_task.id, rtm_id))
+
+ if len(gtg_common) >0:
+ self.update_status("Updating remaining tasks..")
+ self.update_progressbar(0.8)
+ for gtg_id in gtg_common:
+ rtm_id = gtg_to_rtm_id_dict[gtg_id]
+ gtg_task = filterAttr(self.gtg_list, 'id', gtg_id)[0]
+ rtm_task = filterAttr(self.rtm_list, 'id', rtm_id)[0]
+ if rtm_task.modified > gtg_task.modified:
+ self.update_substatus("Updating " + rtm_task.title)
+ gtg_task.copy(rtm_task)
+ else:
+ self.update_substatus("Updating " + gtg_task.title)
+ rtm_task.copy(gtg_task)
+
+ gtg_to_rtm_id_mapping.append((gtg_id, rtm_id))
+
+ self.update_status("Saving current state..")
+ self.update_progressbar(0.9)
+
+ smartSaveToFile(cache_dir, 'gtg_to_rtm_id_mapping',\
+ gtg_to_rtm_id_mapping)
+ #TODO: ask if ok or undo(easy on rtm(see timeline),
+ self.close_gui("Synchronization completed.")
+
+
+ def close_gui(self,msg):
+ self.update_status(msg)
+ self.update_progressbar(1.0)
+ sleep(2)
+ self.update_status("Closing in one second")
+ sleep(1)
+ gobject.idle_add(self.this_plugin.dialog.destroy)
+
+ def update_progressbar(self, percent):
+ self.this_plugin.progressbar_percent = percent
+ gobject.idle_add(self.this_plugin.set_progressbar)
+
+ def update_status(self, status):
+ self.this_plugin.status = status
+ gobject.idle_add(self.this_plugin.set_status)
+
+ def update_substatus(self, substatus):
+ self.this_plugin.substatus = substatus
+ gobject.idle_add(self.this_plugin.set_substatus)
=== added file 'GTG/plugins/rtm_sync/utility.py'
--- GTG/plugins/rtm_sync/utility.py 1970-01-01 00:00:00 +0000
+++ GTG/plugins/rtm_sync/utility.py 2009-09-10 21:37:03 +0000
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 - Luca Invernizzi <invernizzi.l@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+
+import pickle
+import os
+import datetime
+import time
+
+__all__ = ["smartSaveToFile",
+ "smartLoadFromFile",
+ "filterAttr",
+ "iso8601toTime",
+ "timeToIso8601",
+ "dateToIso8601",
+ "unziplist",
+ "timezone"]
+
+
+def smartLoadFromFile(dirname, filename):
+ path=dirname+'/'+filename
+ if os.path.isdir(dirname):
+ if os.path.isfile(path):
+ try:
+ with open(path, 'r') as file:
+ item = pickle.load(file)
+ except:
+ return None
+ return item
+ else:
+ os.makedirs(dirname)
+
+
+def smartSaveToFile(dirname, filename, item, **kwargs):
+ path=dirname+'/'+filename
+ try:
+ with open(path, 'wb') as file:
+ pickle.dump(item, file)
+ except:
+ if kwargs.get('critical', False):
+ raise Exception('saving critical object failed')
+
+
+def unziplist(a):
+ if len(a) == 0:
+ return [], []
+ return tuple(map(list, zip(*a)))
+
+
+def filterAttr(list, attr, value):
+ return filter(lambda elem: getattr(elem, attr) == value, list)
+
+
+def iso8601toTime(string):
+ #FIXME: need to handle time with TIMEZONES!
+ string = string.split('.')[0].split('Z')[0]
+ if string.find('T') == -1:
+ return datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
+ return datetime.datetime.strptime(string.split(".")[0], \
+ "%Y-%m-%dT%H:%M:%S")
+
+
+def timeToIso8601(timeobject):
+ if not hasattr(timeobject, 'strftime'):
+ return ""
+ return timeobject.strftime("%Y-%m-%dT%H:%M:%S")
+
+
+def dateToIso8601(timeobject):
+ if not hasattr(timeobject, 'strftime'):
+ return ""
+ return timeobject.strftime("%Y-%m-%d")
+
+
+def timezone():
+ if time.daylight:
+ return datetime.timedelta(seconds = time.altzone)
+ else:
+ return datetime.timedelta(seconds = time.timezone)
=== modified file 'GTG/tools/taskxml.py'
--- GTG/tools/taskxml.py 2009-08-02 09:12:28 +0000
+++ GTG/tools/taskxml.py 2009-09-09 23:10:48 +0000
@@ -26,6 +26,8 @@
def task_from_xml(task,xmlnode) :
cur_task = task
cur_stat = "%s" %xmlnode.getAttribute("status")
+ uuid = "%s" %xmlnode.getAttribute("uuid")
+ cur_task.set_uuid(uuid)
donedate = cleanxml.readTextNode(xmlnode,"donedate")
cur_task.set_status(cur_stat,donedate=donedate)
#we will fill the task with its content
@@ -51,6 +53,7 @@
content = xml.dom.minidom.parseString(tas)
cur_task.set_text(content.firstChild.toxml()) #pylint: disable-msg=E1103
cur_task.set_due_date(cleanxml.readTextNode(xmlnode,"duedate"))
+ cur_task.set_modified(cleanxml.readTextNode(xmlnode,"modified"))
cur_task.set_start_date(cleanxml.readTextNode(xmlnode,"startdate"))
cur_tags = xmlnode.getAttribute("tags").replace(' ','').split(",")
if "" in cur_tags: cur_tags.remove("")
@@ -65,12 +68,14 @@
t_xml = doc.createElement("task")
t_xml.setAttribute("id",task.get_id())
t_xml.setAttribute("status" , task.get_status())
+ t_xml.setAttribute("uuid" , task.get_uuid())
tags_str = ""
for tag in task.get_tags_name():
tags_str = tags_str + str(tag) + ","
t_xml.setAttribute("tags", tags_str[:-1])
cleanxml.addTextNode(doc,t_xml,"title",task.get_title())
cleanxml.addTextNode(doc,t_xml,"duedate",task.get_due_date())
+ cleanxml.addTextNode(doc,t_xml,"modified",task.get_modified())
cleanxml.addTextNode(doc,t_xml,"startdate",task.get_start_date())
cleanxml.addTextNode(doc,t_xml,"donedate",task.get_closed_date())
childs = task.get_subtask_tids()
=== modified file 'scripts/debug.sh'
--- scripts/debug.sh 2009-07-14 11:40:41 +0000
+++ scripts/debug.sh 2009-09-09 23:10:48 +0000
@@ -1,4 +1,5 @@
#!/bin/bash
export XDG_DATA_HOME="./debug_data/xdg/data"
+export XDG_CACHE_HOME="./debug_data/xdg/cache"
export XDG_CONFIG_HOME="./debug_data/xdg/config"
./gtg