--- /dev/null
+<?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>
--- /dev/null
+#!/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()
--- /dev/null
+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
--- /dev/null
+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()
--- /dev/null
+#!/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)
+
--- /dev/null
+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")
--- /dev/null
+
+"""
+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'), ()]
+ },
+ }
--- /dev/null
+"""
+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
+ ))
--- /dev/null
+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"