Adding the source code (now that I've removed my hard coded credentials)
authorEd Page <epage@Dulcinea.(none)>
Sat, 11 Apr 2009 23:02:19 +0000 (18:02 -0500)
committerEd Page <epage@Dulcinea.(none)>
Sat, 11 Apr 2009 23:02:19 +0000 (18:02 -0500)
16 files changed:
src/doneit.glade [new file with mode: 0644]
src/doneit_glade.py [new file with mode: 0755]
src/gtk_null.py [new file with mode: 0644]
src/gtk_null.pyc [new file with mode: 0644]
src/gtk_rtmilk.py [new file with mode: 0644]
src/gtk_rtmilk.pyc [new file with mode: 0644]
src/gtk_toolbox.py [new file with mode: 0644]
src/gtk_toolbox.pyc [new file with mode: 0644]
src/null.py [new file with mode: 0644]
src/null.pyc [new file with mode: 0644]
src/rtmapi.py [new file with mode: 0644]
src/rtmapi.pyc [new file with mode: 0644]
src/rtmilk.py [new file with mode: 0644]
src/rtmilk.pyc [new file with mode: 0644]
src/toolbox.py [new file with mode: 0644]
src/toolbox.pyc [new file with mode: 0644]

diff --git a/src/doneit.glade b/src/doneit.glade
new file mode 100644 (file)
index 0000000..350ef5c
--- /dev/null
@@ -0,0 +1,592 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.5 on Fri Apr 10 14:52:59 2009 -->
+<glade-interface>
+  <widget class="GtkWindow" id="mainWindow">
+    <property name="default_width">800</property>
+    <property name="default_height">480</property>
+    <child>
+      <widget class="GtkVBox" id="windowParts">
+        <property name="visible">True</property>
+        <child>
+          <widget class="GtkMenuBar" id="mainMenubar">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkMenuItem" id="fileMenuItem">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">_File</property>
+                <property name="use_underline">True</property>
+                <child>
+                  <widget class="GtkMenu" id="fileMenu">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkImageMenuItem" id="connectMenuItem">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">gtk-connect</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkImageMenuItem" id="quitMenuItem">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">gtk-quit</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                        <signal name="activate" handler="on_doneit_quit"/>
+                      </widget>
+                    </child>
+                  </widget>
+                </child>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkMenuItem" id="editMenuItem">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">_Edit</property>
+                <property name="use_underline">True</property>
+                <child>
+                  <widget class="GtkMenu" id="editMenu">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkImageMenuItem" id="pasteMenuItem">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">gtk-paste</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                        <signal name="activate" handler="on_paste"/>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkImageMenuItem" id="deleteMenuItem">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">gtk-delete</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                      </widget>
+                    </child>
+                  </widget>
+                </child>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkMenuItem" id="viewMenuItem">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">_View</property>
+                <property name="use_underline">True</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkMenuItem" id="helpMenuItem">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">_Help</property>
+                <property name="use_underline">True</property>
+                <child>
+                  <widget class="GtkMenu" id="helpMenu">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkImageMenuItem" id="aboutMenuItem">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">gtk-about</property>
+                        <property name="use_underline">True</property>
+                        <property name="use_stock">True</property>
+                        <signal name="activate" handler="on_about"/>
+                      </widget>
+                    </child>
+                  </widget>
+                </child>
+              </widget>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkEventBox" id="errorEventBox">
+            <property name="visible">True</property>
+            <property name="events">GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
+            <child>
+              <widget class="GtkHBox" id="errorBox">
+                <property name="visible">True</property>
+                <child>
+                  <widget class="GtkImage" id="errorImage">
+                    <property name="visible">True</property>
+                    <property name="stock">gtk-dialog-error</property>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="errorDescription">
+                    <property name="visible">True</property>
+                    <property name="use_markup">True</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkImage" id="errorClose">
+                    <property name="visible">True</property>
+                    <property name="stock">gtk-close</property>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </widget>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkVBox" id="todoBox">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkComboBox" id="projectsCombo">
+                <property name="visible">True</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkScrolledWindow" id="todoItemScroll">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                <child>
+                  <widget class="GtkTreeView" id="todoItemTree">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                  </widget>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkHBox" id="add-hbox">
+                <property name="visible">True</property>
+                <child>
+                  <widget class="GtkHButtonBox" id="add-add-hbox">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkButton" id="add-addTaskButton">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="label" translatable="yes">gtk-add</property>
+                        <property name="use_stock">True</property>
+                        <property name="response_id">0</property>
+                      </widget>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkEntry" id="add-taskNameEntry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="has_focus">True</property>
+                    <property name="is_focus">True</property>
+                    <property name="can_default">True</property>
+                    <property name="has_default">True</property>
+                    <property name="receives_default">True</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkHButtonBox" id="add-edit-hbox">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkButton" id="add-pasteTaskNameButton">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="label" translatable="yes">gtk-paste</property>
+                        <property name="use_stock">True</property>
+                        <property name="response_id">0</property>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkButton" id="add-clearTaskNameButton">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="label" translatable="yes">gtk-clear</property>
+                        <property name="use_stock">True</property>
+                        <property name="response_id">0</property>
+                      </widget>
+                      <packing>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+  <widget class="GtkDialog" id="loginDialog">
+    <property name="border_width">5</property>
+    <property name="title" translatable="yes">Login</property>
+    <property name="resizable">False</property>
+    <property name="modal">True</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <property name="skip_taskbar_hint">True</property>
+    <property name="skip_pager_hint">True</property>
+    <property name="deletable">False</property>
+    <property name="transient_for">mainWindow</property>
+    <property name="has_separator">False</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="dialog-vbox1">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkComboBox" id="serviceCombo">
+            <property name="visible">True</property>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <widget class="GtkTable" id="table1">
+            <property name="visible">True</property>
+            <property name="n_rows">2</property>
+            <property name="n_columns">2</property>
+            <child>
+              <widget class="GtkLabel" id="username_label">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Username</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="password_label">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">Password</property>
+              </widget>
+              <packing>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="usernameentry">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="passwordentry">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="visibility">False</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <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="logins_close_button">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-close</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+                <signal name="clicked" handler="on_loginclose_clicked"/>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="loginbutton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="has_default">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+                <signal name="clicked" handler="on_loginbutton_clicked"/>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+  <widget class="GtkDialog" id="editTaskDialog">
+    <property name="border_width">5</property>
+    <property name="title" translatable="yes">Add Task</property>
+    <property name="resizable">False</property>
+    <property name="modal">True</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <property name="skip_taskbar_hint">True</property>
+    <property name="skip_pager_hint">True</property>
+    <property name="deletable">False</property>
+    <property name="transient_for">mainWindow</property>
+    <property name="has_separator">False</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="edit-dialog-vbox2">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkVBox" id="editTaskDialogCoreBox">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkComboBox" id="edit-targetProjectCombo">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkTable" id="edit-coreDetailsGrid">
+                <property name="visible">True</property>
+                <property name="n_rows">3</property>
+                <property name="n_columns">2</property>
+                <child>
+                  <widget class="GtkHBox" id="edit-hbox2">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkEntry" id="edit-taskNameEntry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="has_focus">True</property>
+                        <property name="is_focus">True</property>
+                        <property name="can_default">True</property>
+                        <property name="has_default">True</property>
+                        <property name="receives_default">True</property>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkHButtonBox" id="edit-hbuttonbox2">
+                        <property name="visible">True</property>
+                        <child>
+                          <widget class="GtkButton" id="edit-pasteTaskNameButton">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="label" translatable="yes">gtk-paste</property>
+                            <property name="use_stock">True</property>
+                            <property name="response_id">0</property>
+                          </widget>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkHBox" id="edit-dueDateDisplayControlBox">
+                    <property name="visible">True</property>
+                    <child>
+                      <widget class="GtkEntry" id="edit-dueDateDisplay">
+                        <property name="visible">True</property>
+                        <property name="editable">False</property>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkHButtonBox" id="edit-dueDateActionBox">
+                        <property name="visible">True</property>
+                        <child>
+                          <widget class="GtkButton" id="edit-dueDateProperties">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="label" translatable="yes">gtk-properties</property>
+                            <property name="use_stock">True</property>
+                            <property name="response_id">0</property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkButton" id="edit-clearDueDate">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="label" translatable="yes">gtk-clear</property>
+                            <property name="use_stock">True</property>
+                            <property name="response_id">0</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkComboBox" id="edit-priorityChoiceCombo">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="items" translatable="yes">None
+1
+2
+3</property>
+                  </widget>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                    <property name="y_options"></property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="edit-dueDateLabel">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Due Date</property>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="edit-priorityLabel">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Priority</property>
+                  </widget>
+                  <packing>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="edit-nameLabel">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Name</property>
+                  </widget>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="edit-dialog-action_area2">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="edit-cancelEditTaskButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="edit-commitEditTaskButton">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-save</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>
diff --git a/src/doneit_glade.py b/src/doneit_glade.py
new file mode 100755 (executable)
index 0000000..132155c
--- /dev/null
@@ -0,0 +1,276 @@
+#!/usr/bin/python
+
+
+from __future__ import with_statement
+
+
+import sys
+import gc
+import os
+import threading
+import warnings
+
+import gtk
+import gtk.glade
+
+try:
+       import hildon
+except ImportError:
+       hildon = None
+
+import gtk_toolbox
+
+
+class DoneIt(object):
+
+       __pretty_app_name__ = "DoneIt"
+       __app_name__ = "doneit"
+       __version__ = "0.3.0"
+       __app_magic__ = 0xdeadbeef
+
+       _glade_files = [
+               '/usr/lib/doneit/doneit.glade',
+               os.path.join(os.path.dirname(__file__), "doneit.glade"),
+               os.path.join(os.path.dirname(__file__), "../lib/doneit.glade"),
+       ]
+
+       _user_data = os.path.expanduser("~/.%s/" % __app_name__)
+       _user_settings = "%s/settings.ini" % _user_data
+
+       def __init__(self):
+               self._todoUIs = []
+               self._todoUI = None
+               self._osso = None
+               self._deviceIsOnline = True
+               self._connection = None
+
+               for path in self._glade_files:
+                       if os.path.isfile(path):
+                               self._widgetTree = gtk.glade.XML(path)
+                               break
+               else:
+                       self.display_error_message("Cannot find doneit.glade")
+                       gtk.main_quit()
+               try:
+                       os.makedirs(self._user_data)
+               except OSError, e:
+                       if e.errno != 17:
+                               raise
+
+               self._clipboard = gtk.clipboard_get()
+               self.__window = self._widgetTree.get_widget("mainWindow")
+               self.__errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
+
+               self._app = None
+               self._isFullScreen = False
+               if hildon is not None:
+                       self._app = hildon.Program()
+                       self.__window = hildon.Window()
+                       self._widgetTree.get_widget("mainLayout").reparent(self.__window)
+                       self._app.add_window(self.__window)
+                       self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
+                       self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
+                       self._widgetTree.get_widget("projectsCombo").get_child().set_property('hildon-input-mode', (1 << 4))
+
+                       gtkMenu = self._widgetTree.get_widget("mainMenubar")
+                       menu = gtk.Menu()
+                       for child in gtkMenu.get_children():
+                               child.reparent(menu)
+                       self.__window.set_menu(menu)
+                       gtkMenu.destroy()
+
+                       self.__window.connect("key-press-event", self._on_key_press)
+                       self.__window.connect("window-state-event", self._on_window_state_change)
+               else:
+                       pass # warnings.warn("No Hildon", UserWarning, 2)
+
+               callbackMapping = {
+                       "on_doneit_quit": self._on_close,
+                       "on_paste": self._on_paste,
+                       "on_about": self._on_about_activate,
+               }
+               self._widgetTree.signal_autoconnect(callbackMapping)
+
+               if self.__window:
+                       if hildon is None:
+                               self.__window.set_title("%s" % self.__pretty_app_name__)
+                       self.__window.connect("destroy", self._on_close)
+                       self.__window.show_all()
+
+               backgroundSetup = threading.Thread(target=self._idle_setup)
+               backgroundSetup.setDaemon(True)
+               backgroundSetup.start()
+
+       def _idle_setup(self):
+               # Barebones UI handlers
+               import gtk_null
+               gtk.gdk.threads_enter()
+               try:
+                       self._todoUIs = [
+                               gtk_null.GtkNull(self._widgetTree),
+                       ]
+               finally:
+                       gtk.gdk.threads_leave()
+
+               # Setup maemo specifics
+               try:
+                       import osso
+               except ImportError:
+                       osso = None
+               self._osso = None
+               if osso is not None:
+                       self._osso = osso.Context(DoneIt.__app_name__, DoneIt.__version__, False)
+                       device = osso.DeviceState(self._osso)
+                       device.set_device_state_callback(self._on_device_state_change, 0)
+               else:
+                       pass # warnings.warn("No OSSO", UserWarning, 2)
+
+               try:
+                       import conic
+               except ImportError:
+                       conic = None
+               self._connection = None
+               if conic is not None:
+                       self._connection = conic.Connection()
+                       self._connection.connect("connection-event", self._on_connection_change, self.__app_magic__)
+                       self._connection.request_connection(conic.CONNECT_FLAG_NONE)
+               else:
+                       pass # warnings.warn("No Internet Connectivity API ", UserWarning)
+
+               # Setup costly backends
+               import gtk_rtmilk
+               gtk.gdk.threads_enter()
+               try:
+                       self._todoUIs.extend([
+                               gtk_rtmilk.GtkRtMilk(self._widgetTree),
+                       ])
+                       self._todoUI = self._todoUIs[1]
+                       self._todoUI.enable()
+               finally:
+                       gtk.gdk.threads_leave()
+
+       def display_error_message(self, msg):
+               error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
+
+               def close(dialog, response, editor):
+                       editor.about_dialog = None
+                       dialog.destroy()
+               error_dialog.connect("response", close, self)
+               error_dialog.run()
+
+       def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
+               """
+               For system_inactivity, we have no background tasks to pause
+
+               @note Hildon specific
+               """
+               if memory_low:
+                       gc.collect()
+
+               if save_unsaved_data or shutdown:
+                       pass
+
+       def _on_connection_change(self, connection, event, magicIdentifier):
+               """
+               @note Hildon specific
+               """
+               import conic
+
+               status = event.get_status()
+               error = event.get_error()
+               iap_id = event.get_iap_id()
+               bearer = event.get_bearer_type()
+
+               if status == conic.STATUS_CONNECTED:
+                       self._deviceIsOnline = True
+               elif status == conic.STATUS_DISCONNECTED:
+                       self._deviceIsOnline = False
+
+       def _on_window_state_change(self, widget, event, *args):
+               """
+               @note Hildon specific
+               """
+               if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
+                       self._isFullScreen = True
+               else:
+                       self._isFullScreen = False
+
+       def _on_close(self, *args, **kwds):
+               if self._osso is not None:
+                       self._osso.close()
+
+               try:
+                       pass
+               finally:
+                       gtk.main_quit()
+
+       def _on_paste(self, *args):
+               pass
+
+       def _on_key_press(self, widget, event, *args):
+               """
+               @note Hildon specific
+               """
+               if event.keyval == gtk.keysyms.F6:
+                       if self._isFullScreen:
+                               self.__window.unfullscreen()
+                       else:
+                               self.__window.fullscreen()
+
+       def _on_about_activate(self, *args):
+               dlg = gtk.AboutDialog()
+               dlg.set_name(self.__pretty_app_name__)
+               dlg.set_version(self.__version__)
+               dlg.set_copyright("Copyright 2008 - LGPL")
+               dlg.set_comments("")
+               dlg.set_website("")
+               dlg.set_authors([""])
+               dlg.run()
+               dlg.destroy()
+
+
+def run_doctest():
+       import doctest
+
+       failureCount, testCount = doctest.testmod()
+       if not failureCount:
+               print "Tests Successful"
+               sys.exit(0)
+       else:
+               sys.exit(1)
+
+
+def run_doneit():
+       gtk.gdk.threads_init()
+
+       if hildon is not None:
+               gtk.set_application_name(DoneIt.__pretty_app_name__)
+       handle = DoneIt()
+       gtk.main()
+
+
+class DummyOptions(object):
+
+       def __init__(self):
+               self.test = False
+
+
+if __name__ == "__main__":
+       if len(sys.argv) > 1:
+               try:
+                       import optparse
+               except ImportError:
+                       optparse = None
+
+               if optparse is not None:
+                       parser = optparse.OptionParser()
+                       parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
+                       (commandOptions, commandArgs) = parser.parse_args()
+       else:
+               commandOptions = DummyOptions()
+               commandArgs = []
+
+       if commandOptions.test:
+               run_doctest()
+       else:
+               run_doneit()
diff --git a/src/gtk_null.py b/src/gtk_null.py
new file mode 100644 (file)
index 0000000..04b537c
--- /dev/null
@@ -0,0 +1,46 @@
+import null
+
+
+class GtkNull(object):
+
+       def __init__(self, widgetTree):
+               """
+               @note Thread agnostic
+               """
+               self._todoItemTree = widgetTree.get_widget("todoItemTree")
+               self._todoDetailsTree = widgetTree.get_widget("todoDetailsTree")
+               self._todoDetailsScroll = widgetTree.get_widget("todoDetailsScroll")
+
+               self._completeButton = widgetTree.get_widget("completeButton")
+               self._editButton = widgetTree.get_widget("editButton")
+               self._addButton = widgetTree.get_widget("addButton")
+
+               self._manager = None
+
+       @staticmethod
+       def name():
+               return "None"
+
+       def enable(self):
+               """
+               @note UI Thread
+               """
+               self._manager = null.NullManager("", "")
+
+               self._todoDetailsScroll.hide()
+
+               self._completeButton.set_sensitive(False)
+               self._editButton.set_sensitive(False)
+               self._addButton.set_sensitive(False)
+
+       def disable(self):
+               """
+               @note UI Thread
+               """
+               self._todoDetailsScroll.hide()
+
+               self._completeButton.set_sensitive(True)
+               self._editButton.set_sensitive(True)
+               self._addButton.set_sensitive(True)
+
+               self._manager = None
diff --git a/src/gtk_null.pyc b/src/gtk_null.pyc
new file mode 100644 (file)
index 0000000..c7944bc
Binary files /dev/null and b/src/gtk_null.pyc differ
diff --git a/src/gtk_rtmilk.py b/src/gtk_rtmilk.py
new file mode 100644 (file)
index 0000000..e92c0fb
--- /dev/null
@@ -0,0 +1,322 @@
+import webbrowser
+import datetime
+import urlparse
+
+import gobject
+import gtk
+
+import toolbox
+import gtk_toolbox
+import rtmilk
+import rtmapi
+
+
+def abbreviate(text, expectedLen):
+       singleLine = " ".join(text.split("\n"))
+       lineLen = len(singleLine)
+       if lineLen <= expectedLen:
+               return singleLine
+
+       abbrev = "..."
+
+       leftLen = expectedLen // 2 - 1
+       rightLen = max(expectedLen - leftLen - len(abbrev) + 1, 1)
+
+       abbrevText =  singleLine[0:leftLen] + abbrev + singleLine[-rightLen:-1]
+       assert len(abbrevText) <= expectedLen, "Too long: '%s'" % abbrevText
+       return abbrevText
+
+
+def abbreviate_url(url, domainLength, pathLength):
+       urlParts = urlparse.urlparse(url)
+
+       netloc = urlParts.netloc
+       path = urlParts.path
+
+       pathLength += max(domainLength - len(netloc), 0)
+       domainLength += max(pathLength - len(path), 0)
+
+       netloc = abbreviate(netloc, domainLength)
+       path = abbreviate(path, pathLength)
+       return netloc + path
+
+
+def get_token(username, apiKey, secret):
+       token = None
+       rtm = rtmapi.RTMapi(username, apiKey, secret, token)
+
+       authURL = rtm.getAuthURL()
+       webbrowser.open(authURL)
+       mb = gtk_toolbox.MessageBox2("You need to authorize DoneIt with\nRemember The Milk.\nClick OK after you authorize.")
+       mb.run()
+
+       token = rtm.getToken()
+       return token
+
+
+class GtkRtMilk(object):
+
+       ID_IDX = 0
+       COMPLETION_IDX = 1
+       NAME_IDX = 2
+       PRIORITY_IDX = 3
+       DUE_IDX = 4
+       FUZZY_IDX = 5
+       LINK_IDX = 6
+       NOTES_IDX = 7
+
+       def __init__(self, widgetTree):
+               """
+               @note Thread agnostic
+               """
+               self._clipboard = gtk.clipboard_get()
+
+               self._showCompleted = False
+               self._showIncomplete = True
+
+               self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree)
+
+               self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
+               self._projectsCombo = widgetTree.get_widget("projectsCombo")
+               self._onListActivateId = 0
+
+               self._itemList = gtk.ListStore(
+                       gobject.TYPE_STRING, # id
+                       gobject.TYPE_BOOLEAN,   # is complete
+                       gobject.TYPE_STRING, # name
+                       gobject.TYPE_STRING, # priority
+                       gobject.TYPE_STRING, # due
+                       gobject.TYPE_STRING, # fuzzy due
+                       gobject.TYPE_STRING, # Link
+                       gobject.TYPE_STRING, # Notes
+               )
+               self._completionColumn = gtk.TreeViewColumn('') # Complete?
+               self._completionCell = gtk.CellRendererToggle()
+               self._completionCell.set_property("activatable", True)
+               self._completionCell.connect("toggled", self._on_completion_change)
+               self._completionColumn.pack_start(self._completionCell, False)
+               self._completionColumn.set_attributes(self._completionCell, active=self.COMPLETION_IDX)
+               self._priorityColumn = gtk.TreeViewColumn('') # Priority
+               self._priorityCell = gtk.CellRendererText()
+               self._priorityColumn.pack_start(self._priorityCell, False)
+               self._priorityColumn.set_attributes(self._priorityCell, text=self.PRIORITY_IDX)
+               self._nameColumn = gtk.TreeViewColumn('Name')
+               self._nameCell = gtk.CellRendererText()
+               self._nameColumn.pack_start(self._nameCell, True)
+               self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
+               self._dueColumn = gtk.TreeViewColumn('Due')
+               self._dueCell = gtk.CellRendererText()
+               self._dueColumn.pack_start(self._nameCell, False)
+               self._dueColumn.set_attributes(self._nameCell, text=self.DUE_IDX)
+               self._linkColumn = gtk.TreeViewColumn('') # Link
+               self._linkCell = gtk.CellRendererText()
+               self._linkColumn.pack_start(self._nameCell, False)
+               self._linkColumn.set_attributes(self._nameCell, text=self.LINK_IDX)
+               self._notesColumn = gtk.TreeViewColumn('') # Notes
+               self._notesCell = gtk.CellRendererText()
+               self._notesColumn.pack_start(self._nameCell, False)
+               self._notesColumn.set_attributes(self._nameCell, text=self.NOTES_IDX)
+
+               self._todoItemTree = widgetTree.get_widget("todoItemTree")
+               self._onItemSelectId = 0
+
+               self._taskNameEntry = widgetTree.get_widget("add-taskNameEntry")
+               self._addTaskButton = widgetTree.get_widget("add-addTaskButton")
+               self._pasteTaskNameButton = widgetTree.get_widget("add-pasteTaskNameButton")
+               self._clearTaskNameButton = widgetTree.get_widget("add-clearTaskNameButton")
+               self._onAddId = None
+               self._onAddClickedId = None
+               self._onAddReleasedId = None
+               self._addToEditTimerId = None
+               self._onClearId = None
+               self._onPasteId = None
+
+               self._credentials = gtk_toolbox.LoginWindow(widgetTree)
+               self._manager = None
+
+       @staticmethod
+       def name():
+               return "Remember The Milk"
+
+       def start_session(self):
+               username, password = self._credentials.request_credentials()
+               token = get_token(username, rtmilk.RtMilkManager.API_KEY, rtmilk.RtMilkManager.SECRET)
+               return rtmilk.RtMilkManager(username, password, token)
+
+       def enable(self):
+               """
+               @note UI Thread
+               """
+               self._manager = self.start_session()
+
+               self._projectsList.clear()
+               self._populate_projects()
+
+               self._itemList.clear()
+               self._todoItemTree.append_column(self._completionColumn)
+               self._todoItemTree.append_column(self._priorityColumn)
+               self._todoItemTree.append_column(self._nameColumn)
+               self._todoItemTree.append_column(self._dueColumn)
+               self._todoItemTree.append_column(self._linkColumn)
+               self._todoItemTree.append_column(self._notesColumn)
+               self._reset_task_list()
+
+               self._todoItemTree.set_headers_visible(False)
+               self._nameColumn.set_expand(True)
+
+               self._editDialog.enable(self._manager)
+
+               self._onListActivateId = self._projectsCombo.connect("changed", self._on_list_activate)
+               self._onItemSelectId = self._todoItemTree.connect("row-activated", self._on_item_select)
+               self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
+               self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
+               self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
+               self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
+               self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
+
+       def disable(self):
+               """
+               @note UI Thread
+               """
+               self._projectsCombo.disconnect(self._onListActivateId)
+               self._todoItemTree.disconnect(self._onItemSelectId)
+               self._addTaskButton.disconnect(self._onAddId)
+               self._addTaskButton.disconnect(self._onAddClickedId)
+               self._addTaskButton.disconnect(self._onAddReleasedId)
+               self._pasteTaskNameButton.disconnect(self._onPasteId)
+               self._clearTaskNameButton.disconnect(self._onClearId)
+
+               self._editDialog.disable()
+
+               self._projectsList.clear()
+               self._projectsCombo.set_model(None)
+               self._projectsCombo.disconnect("changed", self._on_list_activate)
+
+               self._todoItemTree.remove_column(self._completionColumn)
+               self._todoItemTree.remove_column(self._priorityColumn)
+               self._todoItemTree.remove_column(self._nameColumn)
+               self._todoItemTree.remove_column(self._dueColumn)
+               self._todoItemTree.remove_column(self._linkColumn)
+               self._todoItemTree.remove_column(self._notesColumn)
+               self._itemList.clear()
+               self._itemList.set_model(None)
+
+               self._manager = None
+
+       def _populate_projects(self):
+               for project in self._manager.get_projects():
+                       projectName = project["name"]
+                       isVisible = project["isVisible"]
+                       row = (projectName, )
+                       if isVisible:
+                               self._projectsList.append(row)
+               self._projectsCombo.set_model(self._projectsList)
+               cell = gtk.CellRendererText()
+               self._projectsCombo.pack_start(cell, True)
+               self._projectsCombo.add_attribute(cell, 'text', 0)
+               self._projectsCombo.set_active(0)
+
+       def _reset_task_list(self):
+               currentProject = self._get_project()
+               projId = self._manager.lookup_project(currentProject)["id"]
+               isMeta = self._manager.get_project(projId)["isMeta"]
+               # @todo RTM handles this by defaulting to a specific list
+               self._addTaskButton.set_sensitive(not isMeta)
+
+               self._itemList.clear()
+               self._populate_items()
+
+       def _get_project(self):
+               currentProjectName = self._projectsCombo.get_active_text()
+               return currentProjectName
+
+       def _populate_items(self):
+               currentProject = self._get_project()
+               projId = self._manager.lookup_project(currentProject)["id"]
+               for taskDetails in self._manager.get_tasks_with_details(projId):
+                       show = self._showCompleted if taskDetails["isCompleted"] else self._showIncomplete
+                       if not show:
+                               continue
+                       id = taskDetails["id"]
+                       isCompleted = taskDetails["isCompleted"]
+                       name = abbreviate(taskDetails["name"], 100)
+                       priority = taskDetails["priority"]
+                       dueDescription = taskDetails["dueDate"]
+                       if dueDescription:
+                               dueDate = datetime.datetime.strptime(dueDescription, "%Y-%m-%dT%H:%M:%SZ")
+                               fuzzyDue = toolbox.to_fuzzy_date(dueDate)
+                       else:
+                               fuzzyDue = ""
+
+                       linkDisplay = taskDetails["url"]
+                       linkDisplay = abbreviate_url(linkDisplay, 20, 10)
+
+                       notes = taskDetails["notes"]
+                       notesDisplay = "%d Notes" % len(notes) if notes else ""
+
+                       row = (id, isCompleted, name, priority, dueDescription, fuzzyDue, linkDisplay, notesDisplay)
+                       self._itemList.append(row)
+               self._todoItemTree.set_model(self._itemList)
+
+       def _on_list_activate(self, *args):
+               self._reset_task_list()
+
+       def _on_item_select(self, treeView, path, viewColumn):
+               taskId = self._itemList[path[0]][self.ID_IDX]
+
+               if viewColumn is self._priorityColumn:
+                       pass
+               elif viewColumn is self._nameColumn:
+                       self._editDialog.request_task(self._manager, taskId)
+                       self._reset_task_list()
+               elif viewColumn is self._dueColumn:
+                       self._editDialog.request_task(self._manager, taskId)
+                       self._reset_task_list()
+               elif viewColumn is self._linkColumn:
+                       webbrowser.open(self._manager.get_task_details(taskId)["url"])
+               elif viewColumn is self._notesColumn:
+                       pass
+
+       def _on_add(self, *args):
+               name = self._taskNameEntry.get_text()
+
+               currentProject = self._get_project()
+               projId = self._manager.lookup_project(currentProject)["id"]
+               taskId = self._manager.add_task(projId, name)
+
+               self._taskNameEntry.set_text("")
+               self._reset_task_list()
+
+       def _on_add_edit(self, *args):
+               name = self._taskNameEntry.get_text()
+
+               currentProject = self._get_project()
+               projId = self._manager.lookup_project(currentProject)["id"]
+               taskId = self._manager.add_task(projId, name)
+
+               try:
+                       self._editDialog.request_task(self._manager, taskId)
+               finally:
+                       self._taskNameEntry.set_text("")
+                       self._reset_task_list()
+
+       def _on_add_pressed(self, widget):
+               self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
+
+       def _on_add_released(self, widget):
+               if self._addToEditTimerId is not None:
+                       gobject.source_remove(self._addToEditTimerId)
+               self._addToEditTimerId = None
+
+       def _on_paste(self, *args):
+               entry = self._taskNameEntry.get_text()
+               entry += self._clipboard.wait_for_text()
+               self._taskNameEntry.set_text(entry)
+
+       def _on_clear(self, *args):
+               self._taskNameEntry.set_text("")
+
+       def _on_completion_change(self, cell, path):
+               taskId = self._itemList[path[0]][self.ID_IDX]
+               self._manager.complete_task(taskId)
+               self._reset_task_list()
diff --git a/src/gtk_rtmilk.pyc b/src/gtk_rtmilk.pyc
new file mode 100644 (file)
index 0000000..f03f1a2
Binary files /dev/null and b/src/gtk_rtmilk.pyc differ
diff --git a/src/gtk_toolbox.py b/src/gtk_toolbox.py
new file mode 100644 (file)
index 0000000..52d29c9
--- /dev/null
@@ -0,0 +1,367 @@
+#!/usr/bin/python
+
+import warnings
+
+import gobject
+import gtk
+
+
+class LoginWindow(object):
+
+       def __init__(self, widgetTree):
+               """
+               @note Thread agnostic
+               """
+               self._dialog = widgetTree.get_widget("loginDialog")
+               self._parentWindow = widgetTree.get_widget("mainWindow")
+               self._serviceCombo = widgetTree.get_widget("serviceCombo")
+               self._usernameEntry = widgetTree.get_widget("usernameentry")
+               self._passwordEntry = widgetTree.get_widget("passwordentry")
+
+               self._serviceList = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING)
+               self._serviceCombo.set_model(self._serviceList)
+               cell = gtk.CellRendererText()
+               self._serviceCombo.pack_start(cell, True)
+               self._serviceCombo.add_attribute(cell, 'text', 1)
+               self._serviceCombo.set_active(0)
+
+               callbackMapping = {
+                       "on_loginbutton_clicked": self._on_loginbutton_clicked,
+                       "on_loginclose_clicked": self._on_loginclose_clicked,
+               }
+               widgetTree.signal_autoconnect(callbackMapping)
+
+       def request_credentials(self, parentWindow = None):
+               """
+               @note UI Thread
+               """
+               if parentWindow is None:
+                       parentWindow = self._parentWindow
+
+               self._serviceCombo.hide()
+               self._serviceList.clear()
+
+               try:
+                       self._dialog.set_transient_for(parentWindow)
+                       self._dialog.set_default_response(gtk.RESPONSE_OK)
+                       response = self._dialog.run()
+                       if response != gtk.RESPONSE_OK:
+                               raise RuntimeError("Login Cancelled")
+
+                       username = self._usernameEntry.get_text()
+                       password = self._passwordEntry.get_text()
+                       self._passwordEntry.set_text("")
+               finally:
+                       self._dialog.hide()
+
+               return username, password
+
+       def request_credentials_from(self, services, parentWindow = None):
+               """
+               @note UI Thread
+               """
+               if parentWindow is None:
+                       parentWindow = self._parentWindow
+
+               self._serviceList.clear()
+               for serviceIdserviceName in services.iteritems():
+                       self._serviceList.append(serviceIdserviceName)
+               self._serviceCombo.set_active(0)
+               self._serviceCombo.show()
+
+               try:
+                       self._dialog.set_transient_for(parentWindow)
+                       self._dialog.set_default_response(gtk.RESPONSE_OK)
+                       response = self._dialog.run()
+                       if response != gtk.RESPONSE_OK:
+                               raise RuntimeError("Login Cancelled")
+
+                       username = self._usernameEntry.get_text()
+                       password = self._passwordEntry.get_text()
+                       self._passwordEntry.set_text("")
+               finally:
+                       self._dialog.hide()
+
+               itr = self._serviceCombo.get_active_iter()
+               serviceId = int(self._serviceList.get_value(itr, 0))
+               self._serviceList.clear()
+               return serviceId, username, password
+
+       def _on_loginbutton_clicked(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+
+       def _on_loginclose_clicked(self, *args):
+               self._dialog.response(gtk.RESPONSE_CANCEL)
+
+
+class ErrorDisplay(object):
+
+       def __init__(self, widgetTree):
+               super(ErrorDisplay, self).__init__()
+               self.__errorBox = widgetTree.get_widget("errorEventBox")
+               self.__errorDescription = widgetTree.get_widget("errorDescription")
+               self.__errorClose = widgetTree.get_widget("errorClose")
+               self.__parentBox = self.__errorBox.get_parent()
+
+               self.__errorBox.connect("button_release_event", self._on_close)
+
+               self.__messages = []
+               self.__parentBox.remove(self.__errorBox)
+
+       def push_message_with_lock(self, message):
+               gtk.gdk.threads_enter()
+               try:
+                       self.push_message(message)
+               finally:
+                       gtk.gdk.threads_leave()
+
+       def push_message(self, message):
+               if 0 < len(self.__messages):
+                       self.__messages.append(message)
+               else:
+                       self.__show_message(message)
+
+       def push_exception(self, exception):
+               self.push_message(exception.message)
+               warnings.warn(exception, stacklevel=3)
+
+       def pop_message(self):
+               if 0 < len(self.__messages):
+                       self.__show_message(self.__messages[0])
+                       del self.__messages[0]
+               else:
+                       self.__hide_message()
+
+       def _on_close(self, *args):
+               self.pop_message()
+
+       def __show_message(self, message):
+               self.__errorDescription.set_text(message)
+               self.__parentBox.pack_start(self.__errorBox, False, False)
+               self.__parentBox.reorder_child(self.__errorBox, 1)
+
+       def __hide_message(self):
+               self.__errorDescription.set_text("")
+               self.__parentBox.remove(self.__errorBox)
+
+
+class MessageBox(gtk.MessageDialog):
+
+       def __init__(self, message):
+               parent = None
+               gtk.MessageDialog.__init__(
+                       self,
+                       parent,
+                       gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
+                       gtk.MESSAGE_ERROR,
+                       gtk.BUTTONS_OK,
+                       message,
+               )
+               self.set_default_response(gtk.RESPONSE_OK)
+               self.connect('response', self._handle_clicked)
+
+       def _handle_clicked(self, *args):
+               self.destroy()
+
+
+class MessageBox2(gtk.MessageDialog):
+
+       def __init__(self, message):
+               parent = None
+               gtk.MessageDialog.__init__(
+                       self,
+                       parent,
+                       gtk.DIALOG_DESTROY_WITH_PARENT,
+                       gtk.MESSAGE_ERROR,
+                       gtk.BUTTONS_OK,
+                       message,
+               )
+               self.set_default_response(gtk.RESPONSE_OK)
+               self.connect('response', self._handle_clicked)
+
+       def _handle_clicked(self, *args):
+               self.destroy()
+
+
+class PopupCalendar(object):
+
+       def __init__(self, parent):
+               self.__calendar = gtk.Calendar()
+               self.__calendar.connect("day-selected-double-click", self._on_date_select)
+
+               self.__popupWindow = gtk.Window(type = gtk.WINDOW_POPUP)
+               self.__popupWindow.set_title("")
+               self.__popupWindow.add(self.__calendar)
+               self.__popupWindow.set_transient_for(parent)
+               self.__popupWindow.set_modal(True)
+
+       def get_date(self):
+               year, month, day = self.__calendar.get_date()
+               month += 1 # Seems to be 0 indexed
+               return year, month, day
+
+       def run(self):
+               self.__popupWindow.show_all()
+
+       def _on_date_select(self, *args):
+               self.__popupWindow.hide()
+               self.callback()
+
+       def callback(self):
+               pass
+
+
+class EditTaskDialog(object):
+
+       def __init__(self, widgetTree):
+               self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
+
+               self._dialog = widgetTree.get_widget("editTaskDialog")
+               self._projectCombo = widgetTree.get_widget("edit-targetProjectCombo")
+               self._taskName = widgetTree.get_widget("edit-taskNameEntry")
+               self._pasteTaskNameButton = widgetTree.get_widget("edit-pasteTaskNameButton")
+               self._priorityChoiceCombo = widgetTree.get_widget("edit-priorityChoiceCombo")
+               self._dueDateDisplay = widgetTree.get_widget("edit-dueDateDisplay")
+               self._dueDateProperties = widgetTree.get_widget("edit-dueDateProperties")
+               self._clearDueDate = widgetTree.get_widget("edit-clearDueDate")
+
+               self._addButton = widgetTree.get_widget("edit-commitEditTaskButton")
+               self._cancelButton = widgetTree.get_widget("edit-cancelEditTaskButton")
+
+               self._popupCalendar = PopupCalendar(self._dialog)
+
+       def enable(self, todoManager):
+               self._populate_projects(todoManager)
+               self._pasteTaskNameButton.connect("clicked", self._on_name_paste)
+               self._dueDateProperties.connect("clicked", self._on_choose_duedate)
+               self._clearDueDate.connect("clicked", self._on_clear_duedate)
+
+               self._addButton.connect("clicked", self._on_add_clicked)
+               self._cancelButton.connect("clicked", self._on_cancel_clicked)
+
+               self._popupCalendar.callback = self._update_duedate
+
+       def disable(self):
+               self._popupCalendar.callback = lambda: None
+
+               self._pasteTaskNameButton.disconnect("clicked", self._on_name_paste)
+               self._dueDateProperties.disconnect("clicked", self._on_choose_duedate)
+               self._clearDueDate.disconnect("clicked", self._on_clear_duedate)
+               self._projectsList.clear()
+               self._projectCombo.set_model(None)
+
+       def request_task(self, todoManager, taskId, parentWindow = None):
+               if parentWindow is not None:
+                       self._dialog.set_transient_for(parentWindow)
+
+               taskDetails = todoManager.get_task_details(taskId)
+               originalProjectId = taskDetails["projId"]
+               originalProjectName = todoManager.get_project(originalProjectId)["name"]
+               originalName = taskDetails["name"]
+               try:
+                       originalPriority = int(taskDetails["priority"])
+               except ValueError:
+                       originalPriority = 0
+               originalDue = taskDetails["due"]
+
+               self._dialog.set_default_response(gtk.RESPONSE_OK)
+               self._taskName.set_text(originalName)
+               self._set_active_proj(originalProjectName)
+               self._priorityChoiceCombo.set_active(originalPriority)
+               self._dueDateDisplay.set_text(originalDue)
+
+               try:
+                       response = self._dialog.run()
+                       if response != gtk.RESPONSE_OK:
+                               raise RuntimeError("Edit Cancelled")
+               finally:
+                       self._dialog.hide()
+
+               newProjectName = self._get_project(todoManager)
+               newName = self._taskName.get_text()
+               newPriority = self._get_priority()
+               newDueDate = self._dueDateDisplay.get_text()
+
+               isProjDifferent = newProjectName != originalProjectName
+               isNameDifferent = newName != originalName
+               isPriorityDifferent = newPriority != originalPriority
+               isDueDifferent = newDueDate != originalDue
+
+               if isProjDifferent:
+                       newProjectId = todoManager.lookup_project(newProjectName)
+                       todoManager.set_project(taskId, newProjectId)
+                       print "PROJ CHANGE"
+               if isNameDifferent:
+                       todoManager.set_name(taskId, newName)
+                       print "NAME CHANGE"
+               if isPriorityDifferent:
+                       todoManager.set_priority(taskId, newPriority)
+                       print "PRIO CHANGE"
+               if isDueDifferent:
+                       todoManager.set_duedate(taskId, newDueDate)
+                       print "DUE CHANGE"
+
+               return {
+                       "projId": isProjDifferent,
+                       "name": isNameDifferent,
+                       "priority": isPriorityDifferent,
+                       "due": isDueDifferent,
+               }
+
+       def _populate_projects(self, todoManager):
+               for projectName in todoManager.get_projects():
+                       row = (projectName["name"], )
+                       self._projectsList.append(row)
+               self._projectCombo.set_model(self._projectsList)
+               cell = gtk.CellRendererText()
+               self._projectCombo.pack_start(cell, True)
+               self._projectCombo.add_attribute(cell, 'text', 0)
+               self._projectCombo.set_active(0)
+
+       def _set_active_proj(self, projName):
+               for i, row in enumerate(self._projectsList):
+                       if row[0] == projName:
+                               self._projectCombo.set_active(i)
+                               break
+               else:
+                       raise ValueError("%s not in list" % projName)
+
+       def _get_project(self, todoManager):
+               name = self._projectCombo.get_active_text()
+               return name
+
+       def _get_priority(self):
+               index = self._priorityChoiceCombo.get_active()
+               assert index != -1
+               if index < 1:
+                       return ""
+               else:
+                       return str(index)
+
+       def _get_date(self):
+               # @bug The date is not used in a consistent manner causing ... issues
+               dateParts = self._popupCalendar.get_date()
+               dateParts = (str(part) for part in dateParts)
+               return "-".join(dateParts)
+
+       def _update_duedate(self):
+               self._dueDateDisplay.set_text(self._get_date())
+
+       def _on_name_paste(self, *args):
+               clipboard = gtk.clipboard_get()
+               contents = clipboard.wait_for_text()
+               if contents is not None:
+                       self._taskName.set_text(contents)
+
+       def _on_choose_duedate(self, *args):
+               self._popupCalendar.run()
+
+       def _on_clear_duedate(self, *args):
+               self._dueDateDisplay.set_text("")
+
+       def _on_add_clicked(self, *args):
+               self._dialog.response(gtk.RESPONSE_OK)
+
+       def _on_cancel_clicked(self, *args):
+               self._dialog.response(gtk.RESPONSE_CANCEL)
+
diff --git a/src/gtk_toolbox.pyc b/src/gtk_toolbox.pyc
new file mode 100644 (file)
index 0000000..e942bdd
Binary files /dev/null and b/src/gtk_toolbox.pyc differ
diff --git a/src/null.py b/src/null.py
new file mode 100644 (file)
index 0000000..808d934
--- /dev/null
@@ -0,0 +1,10 @@
+class NullManager(object):
+
+       def __init__(self, username, password, token=None):
+               pass
+
+       def get_project_names(self):
+               return []
+
+       def save_task(self, taskDesc, projName, priority, duedate=""):
+               raise NotImplementedError("Not logged in to any ToDo system")
diff --git a/src/null.pyc b/src/null.pyc
new file mode 100644 (file)
index 0000000..d23e94b
Binary files /dev/null and b/src/null.pyc differ
diff --git a/src/rtmapi.py b/src/rtmapi.py
new file mode 100644 (file)
index 0000000..0525805
--- /dev/null
@@ -0,0 +1,360 @@
+
+"""
+Python library for Remember The Milk API
+
+@note For help, see http://www.rememberthemilk.com/services/api/methods/
+"""
+
+import weakref
+import warnings
+import urllib
+from md5 import md5
+
+_use_simplejson = False
+try:
+       import simplejson
+       _use_simplejson = True
+except ImportError:
+       pass
+
+
+__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
+
+SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
+AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
+
+
+class RTMError(StandardError):
+       pass
+
+
+class RTMAPIError(RTMError):
+       pass
+
+
+class AuthStateMachine(object):
+       """If the state is in those setup for the machine, then return
+       the datum sent.  Along the way, it is an automatic call if the
+       datum is a method.
+       """
+
+       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 RTMapi(object):
+
+       def __init__(self, userID, apiKey, secret, token=None):
+               self._userID = userID
+               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()
+
+               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(object):
+       "See the `API` structure and `RTM.__init__`"
+
+       def __init__(self, rtm, prefix, methods):
+               self._rtm = weakref.ref(rtm)
+               self._prefix = prefix
+               self._methods = methods
+
+       def __getattr__(self, attr):
+               if attr not in self._methods:
+                       raise AttributeError, 'No such attribute: %s' % attr
+
+               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
+               )
+
+       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)
+
+
+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)
+       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 isinstance(dictionary, dict):
+                       for key, value in dictionary.items():
+                               if isinstance(value, dict):
+                                       value = dottedDict(key, value)
+                               elif isinstance(value, (list, tuple)):
+                                       value = [dottedDict('%s_%d' % (key, i), item)
+                                                        for i, item in enumerate(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 __str__(self):
+               children = [(c, getattr(self, c)) for c in dir(self) if not c.startswith('_')]
+               return '{dotted %s: %s}' % (
+                       self._name,
+                       ', '.join(
+                               ('%s: "%s"' % (k, str(v)))
+                               for (k, v) in children)
+               )
+
+
+def safeEval(string):
+       return eval(string, {}, {})
+
+
+def dottedJSON(json):
+       return dottedDict('ROOT', safeEval(json))
+
+
+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'), ()]
+               },
+       }
diff --git a/src/rtmapi.pyc b/src/rtmapi.pyc
new file mode 100644 (file)
index 0000000..94440fe
Binary files /dev/null and b/src/rtmapi.pyc differ
diff --git a/src/rtmilk.py b/src/rtmilk.py
new file mode 100644 (file)
index 0000000..8f624c6
--- /dev/null
@@ -0,0 +1,278 @@
+"""
+Wrapper for Remember The Milk API
+"""
+
+
+import rtmapi
+
+
+def fix_url(rturl):
+       return "/".join(rturl.split(r"\/"))
+
+
+class RtMilkManager(object):
+       """
+       Interface with rememberthemilk.com
+       """
+       API_KEY = '71f471f7c6ecdda6def341967686fe05'
+       SECRET = '7d3248b085f7efbe'
+
+       def __init__(self, username, password, token):
+               self._username = username
+               self._password = password
+               self._token = token
+
+               self._rtm = rtmapi.RTMapi(self._username, self.API_KEY, self.SECRET, token)
+               self._token = token
+               resp = self._rtm.timelines.create()
+               self._timeline = resp.timeline
+               self._lists = []
+               self._locations = []
+
+       def get_projects(self):
+               if len(self._lists) == 0:
+                       self._populate_projects()
+
+               for list in self._lists:
+                       yield list
+
+       def get_project(self, projId):
+               if len(self._lists) == 0:
+                       self._populate_projects()
+
+               proj = [proj for proj in self._lists if projId == proj["id"]]
+               assert len(proj) == 1, "%r / %r" % (proj, self._lists)
+               return proj[0]
+
+       def get_project_names(self):
+               if len(self._lists) == 0:
+                       self._populate_projects()
+
+               return (list["name"] for list in self._lists)
+
+       def get_locations(self):
+               if len(self._locations) == 0:
+                       self._populate_locations()
+
+               return self._locations
+
+       def lookup_project(self, projName):
+               if len(self._lists) == 0:
+                       self._populate_projects()
+               todoList = [list for list in self._lists if list["name"] == projName]
+               assert len(todoList) == 1, "Wrong number of lists found for %s, in %r" % (projName, todoList)
+               return todoList[0]
+
+       def get_tasks_with_details(self, projId):
+               for taskSeries in self._get_taskseries(projId):
+                       for task in self._get_tasks(taskSeries):
+                               taskId = self._pack_ids(projId, taskSeries.id, task.id)
+                               priority = task.priority if task.priority != "N" else ""
+                               yield {
+                                       "id": taskId,
+                                       "projId": projId,
+                                       "name": taskSeries.name,
+                                       "url": fix_url(taskSeries.url),
+                                       "locationId": taskSeries.location_id,
+                                       "dueDate": task.due,
+                                       "isCompleted": len(task.completed) != 0,
+                                       "completedDate": task.completed,
+                                       "priority": priority,
+                                       "estimate": task.estimate,
+                                       "notes": list(self._get_notes(taskSeries.notes)),
+                               }
+
+       def get_task_details(self, taskId):
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+               for taskSeries in self._get_taskseries(projId):
+                       curSeriesId = taskSeries.id
+                       if seriesId != curSeriesId:
+                               continue
+                       for task in self._get_tasks(taskSeries):
+                               curTaskId = task.id
+                               if task.id != curTaskId:
+                                       continue
+                               return {
+                                       "id": taskId,
+                                       "projId": projId,
+                                       "name": taskSeries.name,
+                                       "url": fix_url(taskSeries.url),
+                                       "locationId": taskSeries.location_id,
+                                       "due": task.due,
+                                       "isCompleted": task.completed,
+                                       "completedDate": task.completed,
+                                       "priority": task.priority,
+                                       "estimate": task.estimate,
+                                       "notes": list(self._get_notes(taskSeries.notes)),
+                               }
+               return {}
+
+       def add_task(self, projId, taskName):
+               rsp = self._rtm.tasks.add(
+                       timeline=self._timeline,
+                       list_id=projId,
+                       name=taskName,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+               seriesId = rsp.list.taskseries.id
+               taskId = rsp.list.taskseries.task.id
+               name = rsp.list.taskseries.name
+
+               return self._pack_ids(projId, seriesId, taskId)
+
+       def set_project(self, taskId, newProjId):
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+               rsp = self._rtm.tasks.setName(
+                       timeline=self._timeline,
+                       from_list_id=projId,
+                       to_list_id=newProjId,
+                       taskseries_id=seriesId,
+                       task_id=taskId,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+
+       def set_name(self, taskId, name):
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+               rsp = self._rtm.tasks.setName(
+                       timeline=self._timeline,
+                       list_id=projId,
+                       taskseries_id=seriesId,
+                       task_id=taskId,
+                       name=name,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+
+       def set_duedate(self, taskId, dueDate):
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+               rsp = self._rtm.tasks.setDueDate(
+                       timeline=self._timeline,
+                       list_id=projId,
+                       taskseries_id=seriesId,
+                       task_id=taskId,
+                       due=dueDate,
+                       parse=1,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+
+       def set_priority(self, taskId, priority):
+               if priority is None:
+                       priority = "N"
+               else:
+                       priority = str(priority)
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+
+               rsp = self._rtm.tasks.setPriority(
+                       timeline=self._timeline,
+                       list_id=projId,
+                       taskseries_id=seriesId,
+                       task_id=taskId,
+                       priority=priority,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+
+       def complete_task(self, taskId):
+               projId, seriesId, taskId = self._unpack_ids(taskId)
+
+               rsp = self._rtm.tasks.complete(
+                       timeline=self._timeline,
+                       list_id=projId,
+                       taskseries_id=seriesId,
+                       task_id=taskId,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+
+       @staticmethod
+       def _pack_ids(*ids):
+               """
+               >>> RtMilkManager._pack_ids(123, 456)
+               '123-456'
+               """
+               return "-".join((str(id) for id in ids))
+
+       @staticmethod
+       def _unpack_ids(ids):
+               """
+               >>> RtMilkManager._unpack_ids("123-456")
+               ['123', '456']
+               """
+               return ids.split("-")
+
+       def _get_taskseries(self, projId):
+               rsp = self._rtm.tasks.getList(
+                       list_id=projId,
+               )
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+               # @note Meta-projects return lists for each project (I think)
+               rspTasksList = rsp.tasks.list
+
+               if not isinstance(rspTasksList, list):
+                       rspTasksList = (rspTasksList, )
+
+               for something in rspTasksList:
+                       try:
+                               something.taskseries
+                       except AttributeError:
+                               continue
+
+                       if isinstance(something.taskseries, list):
+                               somethingsTaskseries = something.taskseries
+                       else:
+                               somethingsTaskseries = (something.taskseries, )
+
+                       for taskSeries in somethingsTaskseries:
+                               yield taskSeries
+
+       def _get_tasks(self, taskSeries):
+               if isinstance(taskSeries.task, list):
+                       tasks = taskSeries.task
+               else:
+                       tasks = (taskSeries.task, )
+               for task in tasks:
+                       yield task
+
+       def _get_notes(self, notes):
+               if not notes:
+                       return
+               elif isinstance(notes.note, list):
+                       notes = notes.note
+               else:
+                       notes = (notes.note, )
+
+               for note in notes:
+                       id = note.id
+                       title = note.title
+                       body = getattr(note, "$t")
+                       yield {
+                               "id": id,
+                               "title": title,
+                               "body": body,
+                       }
+
+       def _populate_projects(self):
+               rsp = self._rtm.lists.getList()
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+               del self._lists[:]
+               self._lists.extend((
+                       dict((
+                               ("name", t.name),
+                               ("id", t.id),
+                               ("isVisible", not int(t.archived)),
+                               ("isMeta", not not int(t.smart)),
+                       ))
+                       for t in rsp.lists.list
+               ))
+
+       def _populate_locations(self):
+               rsp = self._rtm.locations.getList()
+               assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+               del self._locations[:]
+               self._locations.extend((
+                       dict((
+                               ("name", t.name),
+                               ("id", t.id),
+                               ("longitude", t.longitude),
+                               ("latitude", t.latitude),
+                               ("address", t.address),
+                       ))
+                       for t in rsp.locations
+               ))
diff --git a/src/rtmilk.pyc b/src/rtmilk.pyc
new file mode 100644 (file)
index 0000000..c31dc1c
Binary files /dev/null and b/src/rtmilk.pyc differ
diff --git a/src/toolbox.py b/src/toolbox.py
new file mode 100644 (file)
index 0000000..c0e315a
--- /dev/null
@@ -0,0 +1,106 @@
+import sys
+import StringIO
+import urllib
+from xml.dom import minidom
+import datetime
+
+
+def open_anything(source, alternative=None):
+       """URI, filename, or string --> stream
+
+       This function lets you define parsers that take any input source
+       (URL, pathname to local or network file, or actual data as a string)
+       and deal with it in a uniform manner.  Returned object is guaranteed
+       to have all the basic stdio read methods (read, readline, readlines).
+       Just .close() the object when you're done with it.
+
+       Examples:
+       >>> from xml.dom import minidom
+       >>> sock = openAnything("http://localhost/kant.xml")
+       >>> doc = minidom.parse(sock)
+       >>> sock.close()
+       >>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml")
+       >>> doc = minidom.parse(sock)
+       >>> sock.close()
+       >>> sock = openAnything("<ref id='conjunction'><text>and</text><text>or</text></ref>")
+       >>> doc = minidom.parse(sock)
+       >>> sock.close()
+       """
+       if hasattr(source, "read"):
+               return source
+
+       if source == '-':
+               return sys.stdin
+
+       # try to open with urllib (if source is http, ftp, or file URL)
+       try:
+               return urllib.urlopen(source)
+       except (IOError, OSError):
+               ##pass
+               print "ERROR with URL ("+source+")!\n"
+
+       # try to open with native open function (if source is pathname)
+       try:
+               return open(source)
+       except (IOError, OSError):
+               ##pass
+               print "ERROR with file!\n"
+
+       # treat source as string
+       if alternative == None:
+               print 'LAST RESORT.  String is "'+source+'"\n'
+               return StringIO.StringIO(str(source))
+       else:
+               print 'LAST RESORT.  String is "'+alternative+'"\n'
+               return StringIO.StringIO(str(alternative))
+
+
+def load_xml(source, alternative=None):
+       """load XML input source, return parsed XML document
+
+       - a URL of a remote XML file ("http://diveintopython.org/kant.xml")
+       - a filename of a local XML file ("~/diveintopython/common/py/kant.xml")
+       - standard input ("-")
+       - the actual XML document, as a string
+       """
+       sock = open_anything(source, alternative)
+       try:
+               xmldoc = minidom.parse(sock).documentElement
+       except (IOError, OSError):
+               print "ERROR with data"
+               sock.close()
+               sock = open_anything('<response method="getProjects"><project projName="ERROR!"/></response>')
+               xmldoc = minidom.parse(sock).documentElement
+       sock.close()
+       return xmldoc
+
+
+def to_fuzzy_date(targetDate, todaysDate = datetime.datetime.today()):
+       """
+       Conert a date/time/datetime object to a fuzzy date
+
+       @bug Not perfect, but good enough for now
+       """
+       delta = targetDate - todaysDate
+       days = abs(delta.days)
+       directionBy1 = "Next" if 0 < delta.days else "Last"
+       directionByN = "Later" if 0 < delta.days else "Earlier"
+       directionByInf = "from now" if 0 < delta.days else "ago"
+       if 2*365 < days:
+               return "Forever %s" % directionByInf
+       elif 365 < days:
+               return "%s year" % directionBy1
+       elif 2*30 < days:
+               return "%s this year" % directionByN
+       elif 30 < days:
+               return "%s month" % directionBy1
+       elif 14 < days:
+               return "%s this month" % directionByN
+       elif 7 < days:
+               return "%s week" % directionBy1
+       elif 2 < days:
+               return "%s this week" % directionByN
+       elif 1 < days:
+               return "%s day" % directionByN
+       else:
+               return "Today"
diff --git a/src/toolbox.pyc b/src/toolbox.pyc
new file mode 100644 (file)
index 0000000..82665df
Binary files /dev/null and b/src/toolbox.pyc differ