From df62a177f0b1177e7e16580ed7e065fa19f2b69c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 13 Apr 2009 21:55:54 -0500 Subject: [PATCH] Renaming files similar to how I run DialCentral --- src/doneit_glade.py | 8 +- src/gtk_null.py | 43 ------ src/gtk_rtmilk.py | 374 --------------------------------------------------- src/null.py | 10 -- src/null_backend.py | 10 ++ src/null_view.py | 43 ++++++ src/rtm_backend.py | 278 ++++++++++++++++++++++++++++++++++++++ src/rtm_view.py | 374 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/rtmilk.py | 278 -------------------------------------- 9 files changed, 709 insertions(+), 709 deletions(-) delete mode 100644 src/gtk_null.py delete mode 100644 src/gtk_rtmilk.py delete mode 100644 src/null.py create mode 100644 src/null_backend.py create mode 100644 src/null_view.py create mode 100644 src/rtm_backend.py create mode 100644 src/rtm_view.py delete mode 100644 src/rtmilk.py diff --git a/src/doneit_glade.py b/src/doneit_glade.py index eef0c6f..587adbc 100755 --- a/src/doneit_glade.py +++ b/src/doneit_glade.py @@ -106,9 +106,9 @@ class DoneIt(object): 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() @@ -140,9 +140,9 @@ class DoneIt(object): 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() diff --git a/src/gtk_null.py b/src/gtk_null.py deleted file mode 100644 index ab33b0b..0000000 --- a/src/gtk_null.py +++ /dev/null @@ -1,43 +0,0 @@ -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) diff --git a/src/gtk_rtmilk.py b/src/gtk_rtmilk.py deleted file mode 100644 index 693cc62..0000000 --- a/src/gtk_rtmilk.py +++ /dev/null @@ -1,374 +0,0 @@ -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() diff --git a/src/null.py b/src/null.py deleted file mode 100644 index 808d934..0000000 --- a/src/null.py +++ /dev/null @@ -1,10 +0,0 @@ -class NullManager(object): - - def __init__(self, username, password, token=None): - pass - - def get_project_names(self): - return [] - - def save_task(self, taskDesc, projName, priority, duedate=""): - raise NotImplementedError("Not logged in to any ToDo system") diff --git a/src/null_backend.py b/src/null_backend.py new file mode 100644 index 0000000..808d934 --- /dev/null +++ b/src/null_backend.py @@ -0,0 +1,10 @@ +class NullManager(object): + + def __init__(self, username, password, token=None): + pass + + def get_project_names(self): + return [] + + def save_task(self, taskDesc, projName, priority, duedate=""): + raise NotImplementedError("Not logged in to any ToDo system") diff --git a/src/null_view.py b/src/null_view.py new file mode 100644 index 0000000..d1770d2 --- /dev/null +++ b/src/null_view.py @@ -0,0 +1,43 @@ +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) diff --git a/src/rtm_backend.py b/src/rtm_backend.py new file mode 100644 index 0000000..0bafda1 --- /dev/null +++ b/src/rtm_backend.py @@ -0,0 +1,278 @@ +""" +Wrapper for Remember The Milk API +""" + + +import rtmapi + + +def fix_url(rturl): + return "/".join(rturl.split(r"\/")) + + +class RtMilkManager(object): + """ + Interface with rememberthemilk.com + + @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 + )) diff --git a/src/rtm_view.py b/src/rtm_view.py new file mode 100644 index 0000000..b986b3c --- /dev/null +++ b/src/rtm_view.py @@ -0,0 +1,374 @@ +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() diff --git a/src/rtmilk.py b/src/rtmilk.py deleted file mode 100644 index 0bafda1..0000000 --- a/src/rtmilk.py +++ /dev/null @@ -1,278 +0,0 @@ -""" -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 - )) -- 1.7.9.5