def _idle_setup(self):
# Barebones UI handlers
- import gtk_null
+ import null_view
with gtk_toolbox.gtk_lock():
- nullView = gtk_null.GtkNull(self._widgetTree)
+ nullView = null_view.GtkNull(self._widgetTree)
self._todoUIs[nullView.name()] = nullView
self._todoUI = nullView
self._todoUI.enable()
pass # warnings.warn("No Internet Connectivity API ", UserWarning)
# Setup costly backends
- import gtk_rtmilk
+ import rtm_view
with gtk_toolbox.gtk_lock():
- rtmView = gtk_rtmilk.GtkRtMilk(self._widgetTree)
+ rtmView = rtm_view.GtkRtMilk(self._widgetTree)
self._todoUIs[rtmView.name()] = rtmView
self._defaultUIName = rtmView.name()
+++ /dev/null
-import null
-
-
-class GtkNull(object):
-
- def __init__(self, widgetTree):
- """
- @note Thread agnostic
- """
- self._projectsCombo = widgetTree.get_widget("projectsCombo")
- self._addTaskButton = widgetTree.get_widget("add-addTaskButton")
-
- self._manager = null.NullManager("", "")
-
- @staticmethod
- def name():
- return "None"
-
- def load_settings(self, config):
- pass
-
- def save_settings(self, config):
- pass
-
- def login(self):
- pass
-
- def logout(self):
- pass
-
- def enable(self):
- """
- @note UI Thread
- """
- self._projectsCombo.set_sensitive(False)
- self._addTaskButton.set_sensitive(False)
-
- def disable(self):
- """
- @note UI Thread
- """
- self._projectsCombo.set_sensitive(True)
- self._addTaskButton.set_sensitive(True)
+++ /dev/null
-import webbrowser
-import datetime
-import urlparse
-import base64
-
-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
-
-
-def get_credentials(credentialsDialog):
- username, password = credentialsDialog.request_credentials()
- token = get_token(username, rtmilk.RtMilkManager.API_KEY, rtmilk.RtMilkManager.SECRET)
- return username, password, 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
-
- # @todo Need to figure out how to make the list sortable, especially with weird priority sorting
- 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.FUZZY_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._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree)
- self._credentials = "", "", ""
- self._manager = None
-
- @staticmethod
- def name():
- return "Remember The Milk"
-
- def load_settings(self, config):
- """
- @note Thread Agnostic
- """
- blobs = (
- config.get(self.name(), "bin_blob_%i" % i)
- for i in xrange(len(self._credentials))
- )
- creds = (
- base64.b64decode(blob)
- for blob in blobs
- )
- self._credentials = tuple(creds)
-
- def save_settings(self, config):
- """
- @note Thread Agnostic
- """
- config.add_section(self.name())
- for i, value in enumerate(self._credentials):
- blob = base64.b64encode(value)
- config.set(self.name(), "bin_blob_%i" % i, blob)
-
- def login(self):
- """
- @note UI Thread
- """
- if self._manager is not None:
- return
-
- credentials = self._credentials
- while True:
- try:
- self._manager = rtmilk.RtMilkManager(*credentials)
- self._credentials = credentials
- return # Login succeeded
- except rtmapi.AuthStateMachine.NoData:
- # Login failed, grab new credentials
- credentials = get_credentials(self._credentialsDialog)
-
- def logout(self):
- """
- @note Thread Agnostic
- """
- self._credentials = "", "", ""
- self._manager = None
-
- def enable(self):
- """
- @note UI Thread
- """
- 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):
- # @todo Look into using a button for notes and links, and labels for all else
- 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):
- # @todo See if there is a way to get a right click / tap'n'hold for more task goodness
- # https://garage.maemo.org/plugins/wiki/index.php?TapAndHold&id=40&type=g
- 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
-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
+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
+import null_backend
+
+
+class GtkNull(object):
+
+ def __init__(self, widgetTree):
+ """
+ @note Thread agnostic
+ """
+ self._projectsCombo = widgetTree.get_widget("projectsCombo")
+ self._addTaskButton = widgetTree.get_widget("add-addTaskButton")
+
+ self._manager = null_backend.NullManager("", "")
+
+ @staticmethod
+ def name():
+ return "None"
+
+ def load_settings(self, config):
+ pass
+
+ def save_settings(self, config):
+ pass
+
+ def login(self):
+ pass
+
+ def logout(self):
+ pass
+
+ def enable(self):
+ """
+ @note UI Thread
+ """
+ self._projectsCombo.set_sensitive(False)
+ self._addTaskButton.set_sensitive(False)
+
+ def disable(self):
+ """
+ @note UI Thread
+ """
+ self._projectsCombo.set_sensitive(True)
+ self._addTaskButton.set_sensitive(True)
--- /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
+
+ @todo Decide upon an interface that will end up a bit less bloated
+ @todo Add interface for task tags
+ @todo Add interface for postponing tasks (have way for UI to specify how many days to postpone?)
+ @todo Add interface for task recurrence
+ @todo Add interface for task estimate
+ @todo Add interface for task location
+ @todo Add interface for task url
+ @todo Add interface for task notes
+ @todo Add undo support
+ """
+ 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 = []
+
+ def get_projects(self):
+ if len(self._lists) == 0:
+ self._populate_projects()
+
+ for list in self._lists:
+ yield list
+
+ def get_project(self, projId):
+ proj = [proj for proj in self.get_projects() if projId == proj["id"]]
+ assert len(proj) == 1, "%r / %r" % (proj, self._lists)
+ return proj[0]
+
+ def get_project_names(self):
+ return (list["name"] for list in self.get_projects)
+
+ def lookup_project(self, projName):
+ """
+ From a project's name, returns the project's details
+ """
+ todoList = [list for list in self.get_projects() if list["name"] == projName]
+ assert len(todoList) == 1, "Wrong number of lists found for %s, in %r" % (projName, todoList)
+ return todoList[0]
+
+ def get_locations(self):
+ rsp = self._rtm.locations.getList()
+ assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
+ locations = [
+ dict((
+ ("name", t.name),
+ ("id", t.id),
+ ("longitude", t.longitude),
+ ("latitude", t.latitude),
+ ("address", t.address),
+ ))
+ for t in rsp.locations
+ ]
+ return locations
+
+ def get_tasks_with_details(self, projId):
+ for realProjId, taskSeries in self._get_taskseries(projId):
+ for task in self._get_tasks(taskSeries):
+ taskId = self._pack_ids(realProjId, 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 realProjId, taskSeries in self._get_taskseries(projId):
+ assert projId == realProjId, "%s != %s, looks like we let leak a metalist id when packing a task id" % (projId, realProjId)
+ 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": realProjId,
+ "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.moveTo(
+ 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:
+ realProjId = something.id
+ try:
+ something.taskseries
+ except AttributeError:
+ continue
+
+ if isinstance(something.taskseries, list):
+ somethingsTaskseries = something.taskseries
+ else:
+ somethingsTaskseries = (something.taskseries, )
+
+ for taskSeries in somethingsTaskseries:
+ yield realProjId, 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
+ ))
--- /dev/null
+import webbrowser
+import datetime
+import urlparse
+import base64
+
+import gobject
+import gtk
+
+import toolbox
+import gtk_toolbox
+import rtm_backend
+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
+
+
+def get_credentials(credentialsDialog):
+ username, password = credentialsDialog.request_credentials()
+ token = get_token(username, rtm_backend.RtMilkManager.API_KEY, rtm_backend.RtMilkManager.SECRET)
+ return username, password, 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
+
+ # @todo Need to figure out how to make the list sortable, especially with weird priority sorting
+ 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.FUZZY_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._credentialsDialog = gtk_toolbox.LoginWindow(widgetTree)
+ self._credentials = "", "", ""
+ self._manager = None
+
+ @staticmethod
+ def name():
+ return "Remember The Milk"
+
+ def load_settings(self, config):
+ """
+ @note Thread Agnostic
+ """
+ blobs = (
+ config.get(self.name(), "bin_blob_%i" % i)
+ for i in xrange(len(self._credentials))
+ )
+ creds = (
+ base64.b64decode(blob)
+ for blob in blobs
+ )
+ self._credentials = tuple(creds)
+
+ def save_settings(self, config):
+ """
+ @note Thread Agnostic
+ """
+ config.add_section(self.name())
+ for i, value in enumerate(self._credentials):
+ blob = base64.b64encode(value)
+ config.set(self.name(), "bin_blob_%i" % i, blob)
+
+ def login(self):
+ """
+ @note UI Thread
+ """
+ if self._manager is not None:
+ return
+
+ credentials = self._credentials
+ while True:
+ try:
+ self._manager = rtm_backend.RtMilkManager(*credentials)
+ self._credentials = credentials
+ return # Login succeeded
+ except rtmapi.AuthStateMachine.NoData:
+ # Login failed, grab new credentials
+ credentials = get_credentials(self._credentialsDialog)
+
+ def logout(self):
+ """
+ @note Thread Agnostic
+ """
+ self._credentials = "", "", ""
+ self._manager = None
+
+ def enable(self):
+ """
+ @note UI Thread
+ """
+ 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):
+ # @todo Look into using a button for notes and links, and labels for all else
+ 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):
+ # @todo See if there is a way to get a right click / tap'n'hold for more task goodness
+ # https://garage.maemo.org/plugins/wiki/index.php?TapAndHold&id=40&type=g
+ 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
-"""
-Wrapper for Remember The Milk API
-"""
-
-
-import rtmapi
-
-
-def fix_url(rturl):
- return "/".join(rturl.split(r"\/"))
-
-
-class RtMilkManager(object):
- """
- Interface with rememberthemilk.com
-
- @todo Decide upon an interface that will end up a bit less bloated
- @todo Add interface for task tags
- @todo Add interface for postponing tasks (have way for UI to specify how many days to postpone?)
- @todo Add interface for task recurrence
- @todo Add interface for task estimate
- @todo Add interface for task location
- @todo Add interface for task url
- @todo Add interface for task notes
- @todo Add undo support
- """
- 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 = []
-
- def get_projects(self):
- if len(self._lists) == 0:
- self._populate_projects()
-
- for list in self._lists:
- yield list
-
- def get_project(self, projId):
- proj = [proj for proj in self.get_projects() if projId == proj["id"]]
- assert len(proj) == 1, "%r / %r" % (proj, self._lists)
- return proj[0]
-
- def get_project_names(self):
- return (list["name"] for list in self.get_projects)
-
- def lookup_project(self, projName):
- """
- From a project's name, returns the project's details
- """
- todoList = [list for list in self.get_projects() if list["name"] == projName]
- assert len(todoList) == 1, "Wrong number of lists found for %s, in %r" % (projName, todoList)
- return todoList[0]
-
- def get_locations(self):
- rsp = self._rtm.locations.getList()
- assert rsp.stat == "ok", "Bad response: %r" % (rsp, )
- locations = [
- dict((
- ("name", t.name),
- ("id", t.id),
- ("longitude", t.longitude),
- ("latitude", t.latitude),
- ("address", t.address),
- ))
- for t in rsp.locations
- ]
- return locations
-
- def get_tasks_with_details(self, projId):
- for realProjId, taskSeries in self._get_taskseries(projId):
- for task in self._get_tasks(taskSeries):
- taskId = self._pack_ids(realProjId, 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 realProjId, taskSeries in self._get_taskseries(projId):
- assert projId == realProjId, "%s != %s, looks like we let leak a metalist id when packing a task id" % (projId, realProjId)
- 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": realProjId,
- "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.moveTo(
- 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:
- realProjId = something.id
- try:
- something.taskseries
- except AttributeError:
- continue
-
- if isinstance(something.taskseries, list):
- somethingsTaskseries = something.taskseries
- else:
- somethingsTaskseries = (something.taskseries, )
-
- for taskSeries in somethingsTaskseries:
- yield realProjId, 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
- ))