From d4722fc6ac1ce29aec584516940169487eefbfd0 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 26 Jul 2015 14:16:47 -0700 Subject: [PATCH 1/5] Name and description can be edited from details view Closes #42223443476018 #42238129502571 --- cmdasana.py | 28 +++++++++++++++++++++++----- ui.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/cmdasana.py b/cmdasana.py index bf91608..9d170c7 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -130,6 +130,9 @@ class CmdAsana: def updateTask(self, task_id, name): self.client.tasks.update(task_id, name=name) + + def updateDetails(self, task_id, details): + self.client.tasks.update(task_id, notes=details) def addComment(self, task_id, comment): self.client.stories.create_on_task(task_id, {"text": comment}) @@ -178,8 +181,7 @@ class CmdAsana: task = self.client.tasks.find_by_id(task_id) stories = self.client.stories.find_by_task(task_id) task_details = ui.TaskDetails(task, stories) - urwid.connect_signal(task_details, 'comment', self.addComment) - urwid.connect_signal(task_details, 'loadproject', self.showProject) + self.connectDetailsSignals(task_details) self.replaceBody(task_details) def registerSignals(self): @@ -187,18 +189,28 @@ class CmdAsana: 'complete', 'newtask', 'updatetask', - 'details' + 'details', ]) urwid.register_signal(ui.TaskEdit, [ 'complete', 'newtask', 'updatetask', - 'details' + 'details', + ]) + + urwid.register_signal(ui.TaskDetails, [ + 'comment', + 'loadproject', + 'updatedescription', + 'updatetask', ]) - urwid.register_signal(ui.TaskDetails, ['comment', 'loadproject']) urwid.register_signal(ui.CommentEdit, ['comment']) + urwid.register_signal(ui.DescriptionEdit, ['updatedescription']) + + urwid.register_signal(ui.TaskNameEdit, 'updatetask') + urwid.register_signal(ui.WorkspaceMenu, 'click') urwid.register_signal(ui.ProjectList, 'loadproject') @@ -215,6 +227,12 @@ class CmdAsana: urwid.connect_signal(task_list, 'updatetask', self.updateTask) urwid.connect_signal(task_list, 'details', self.showDetails) + def connectDetailsSignals(self, task_details): + urwid.connect_signal(task_details, 'comment', self.addComment) + urwid.connect_signal(task_details, 'loadproject', self.showProject) + urwid.connect_signal(task_details, 'updatedescription', self.updateDetails) + urwid.connect_signal(task_details, 'updatetask', self.updateTask) + def handleInput(self, key): if key in ('q', 'Q'): raise urwid.ExitMainLoop() diff --git a/ui.py b/ui.py index 376171d..6346129 100644 --- a/ui.py +++ b/ui.py @@ -10,6 +10,7 @@ palette = [ ('selected', 'standout', ''), ('selected workspace', 'standout,bold', ''), ('header', 'bold,light green', ''), + ('secondary', 'light gray', ''), ('task', 'light green', ''), ('section', 'white', 'dark green'), ('workspace', 'white', 'dark blue'), @@ -193,13 +194,40 @@ class TaskEdit(urwid.Edit): class CommentEdit(urwid.Edit): def __init__(self, task): self.task = task - super(CommentEdit, self).__init__('Add a comment:\n') + super(CommentEdit, self).__init__(('secondary', 'Add a comment:\n')) def keypress(self, size, key): if key != 'enter': return super(CommentEdit, self).keypress(size, key) urwid.emit_signal(self, 'comment', self.task['id'], self.edit_text) +class TaskNameEdit(urwid.Edit): + def __init__(self, task): + self.task = task + super(TaskNameEdit, self).__init__(('secondary', + '#' + str(task['id']) + " "), + task['name']) + + def keypress(self, size, key): + if key in ('enter', 'esc', 'up', 'down'): + if (self.edit_text != self.task['name']): + urwid.emit_signal(self, 'updatetask', self.task['id'], + self.edit_text) + return super(TaskNameEdit, self).keypress(size, key) + +class DescriptionEdit(urwid.Edit): + def __init__(self, task): + self.task = task + super(DescriptionEdit, self).__init__(('secondary', 'Description:\n'), + task['notes'], + multiline=True) + + def keypress(self, size, key): + if key != 'esc': + return super(DescriptionEdit, self).keypress(size, key) + urwid.emit_signal(self, 'updatedescription', self.task['id'], + self.edit_text) + class TaskDetails(urwid.Pile): def __init__(self, task, stories): self.task = task @@ -208,23 +236,30 @@ class TaskDetails(urwid.Pile): comment_edit = CommentEdit(task) urwid.connect_signal(comment_edit, 'comment', self.comment) + description_edit = DescriptionEdit(task) + urwid.connect_signal(description_edit, 'updatedescription', + self.updateDescription) + + task_name_edit = TaskNameEdit(task) + urwid.connect_signal(task_name_edit, 'updatetask', self.updateTask) + projects = [('pack', ProjectIcon(project, self.loadProject)) for project in task['projects']] if task['assignee']: assignee = urwid.Text('Assigned to: ' + task['assignee']['name']) else: - assignee = urwid.Text('(not assigned)') + assignee = urwid.Text(('secondary', '(not assigned)')) body = projects + \ [ ('pack', urwid.Divider('=')), - ('pack', urwid.Text(('header', task['name'] + \ - " #" + str(task['id'])))), + ('pack', task_name_edit), ('pack', assignee), ('pack', urwid.Divider('-')), - ('pack', urwid.Text(task['notes'])), + ('pack', description_edit), + ('pack', urwid.Divider('-')), ] + \ [('pack', urwid.Text('[' + story['created_by']['name'] + '] ' + \ story['text'])) for story in stories] + \ @@ -237,5 +272,11 @@ class TaskDetails(urwid.Pile): def comment(self, task_id, comment): urwid.emit_signal(self, 'comment', task_id, comment) + def updateDescription(self, task_id, description): + urwid.emit_signal(self, 'updatedescription', task_id, description) + + def updateTask(self, task_id, name): + urwid.emit_signal(self, 'updatetask', task_id, name) + def loadProject(self, project_id): urwid.emit_signal(self, 'loadproject', project_id) From d370674e10c5a1d7c534015e894c3b38c5eb9bfc Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 26 Jul 2015 16:41:38 -0700 Subject: [PATCH 2/5] Threads most api requests Fixes #42358460560357 --- cmdasana.py | 152 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 112 insertions(+), 40 deletions(-) diff --git a/cmdasana.py b/cmdasana.py index 9d170c7..9d0705a 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -3,6 +3,7 @@ import os import sys import json +from threading import Thread import urwid import asana @@ -16,6 +17,8 @@ from secrets import CLIENT_ID, CLIENT_SECRET PERSONAL = 498346170860 class CmdAsana: + loop = None + def __init__(self): try: f = open(".oauth", "r") @@ -110,33 +113,52 @@ class CmdAsana: self.client.tasks.update(task_id, completed=True) def newTask(self, task_after_id): - if self.state['view'] == 'project': - task = self.client.tasks.create_in_workspace( - self.state['workspace_id'], - projects=[self.state['id']] - ) - if task_after_id != None: - self.client.tasks.add_project(task['id'], - project=self.state['id'], - insert_after=task_after_id) - else: - task = self.client.tasks.create_in_workspace( - self.state['workspace_id'], - assignee=self.me['id'] - ) + def runInThread(): + if self.state['view'] == 'project': + task = self.client.tasks.create_in_workspace( + self.state['workspace_id'], + projects=[self.state['id']] + ) + if task_after_id != None: + self.client.tasks.add_project(task['id'], + project=self.state['id'], + insert_after=task_after_id) + else: + task = self.client.tasks.create_in_workspace( + self.state['workspace_id'], + assignee=self.me['id'] + ) - task_list,_ = self.frame.contents[1] - task_list.insertNewTask(task) + update(task) + + def update(task): + task_list,_ = self.frame.contents[1] + task_list.insertNewTask(task) + + thread = Thread(target=runInThread) + thread.start() def updateTask(self, task_id, name): - self.client.tasks.update(task_id, name=name) + def runInThread(): + self.client.tasks.update(task_id, name=name) + + thread = Thread(target=runInThread) + thread.start() - def updateDetails(self, task_id, details): - self.client.tasks.update(task_id, notes=details) + def updateDescription(self, task_id, description): + def runInThread(): + self.client.tasks.update(task_id, notes=description) + + thread = Thread(target=runInThread) + thread.start() def addComment(self, task_id, comment): - self.client.stories.create_on_task(task_id, {"text": comment}) - self.showDetails(task_id) + def runInThread(): + self.client.stories.create_on_task(task_id, {"text": comment}) + self.showDetails(task_id, show_loading=False) + + thread = Thread(target=runInThread) + thread.start() def replaceBody(self, widget): old_widget,_ = self.frame.contents.pop() @@ -144,15 +166,31 @@ class CmdAsana: self.clearSignals(old_widget) self.frame.contents.append((widget, self.frame.options())) self.frame.focus_position = 0 + if self.loop != None: + self.loop.draw_screen() + + def showMainLoading(self): + text = urwid.Text(('loading', '[loading...]')) + self.replaceBody(urwid.Filler(text)) def showMyTasks(self, workspace_id): self.state['view'] = 'atm' self.state['id'] = workspace_id self.state['workspace_id'] = workspace_id - task_list = ui.TaskList(self.allMyTasks(workspace_id)) - self.connectTaskListSignals(task_list) - self.replaceBody(task_list) + self.showMainLoading() + + def runInThread(): + tasks = self.allMyTasks(workspace_id) + update(tasks) + + def update(tasks): + task_list = ui.TaskList(tasks) + self.connectTaskListSignals(task_list) + self.replaceBody(task_list) + + thread = Thread(target=runInThread) + thread.start() def showProject(self, project_id): if project_id == None: @@ -160,29 +198,59 @@ class CmdAsana: self.state['view'] = 'project' self.state['id'] = project_id - task_list = ui.TaskList(self.projectTasks(project_id)) - self.connectTaskListSignals(task_list) - self.replaceBody(task_list) + self.showMainLoading() + + def runInThread(): + tasks = self.projectTasks(project_id) + update(tasks) + + def update(tasks): + task_list = ui.TaskList(tasks) + self.connectTaskListSignals(task_list) + self.replaceBody(task_list) + + thread = Thread(target=runInThread) + thread.start() def showProjectList(self, workspace_id): self.state['view'] = 'workspace' self.state['id'] = workspace_id self.state['workspace_id'] = workspace_id - self.workspace_id = workspace_id - project_list = ui.ProjectList(self.allMyProjects()) - urwid.connect_signal(project_list, 'loadproject', self.showProject) - self.replaceBody(project_list) - def showDetails(self, task_id): + self.showMainLoading() + + def runInThread(): + projects = self.allMyProjects() + update(projects) + + def update(projects): + project_list = ui.ProjectList(projects) + urwid.connect_signal(project_list, 'loadproject', self.showProject) + self.replaceBody(project_list) + + thread = Thread(target=runInThread) + thread.start() + + def showDetails(self, task_id, show_loading=True): self.state['view'] = 'details' self.state['id'] = task_id - task = self.client.tasks.find_by_id(task_id) - stories = self.client.stories.find_by_task(task_id) - task_details = ui.TaskDetails(task, stories) - self.connectDetailsSignals(task_details) - self.replaceBody(task_details) + if show_loading: + self.showMainLoading() + + def runInThread(): + task = self.client.tasks.find_by_id(task_id) + stories = self.client.stories.find_by_task(task_id) + update(task, stories) + + def update(task, stories): + task_details = ui.TaskDetails(task, stories) + self.connectDetailsSignals(task_details) + self.replaceBody(task_details) + + thread = Thread(target=runInThread) + thread.start() def registerSignals(self): urwid.register_signal(ui.TaskList, [ @@ -220,6 +288,9 @@ class CmdAsana: urwid.disconnect_signal(widget, 'newtask', self.newTask) urwid.disconnect_signal(widget, 'updatetask', self.updateTask) urwid.disconnect_signal(widget, 'details', self.showDetails) + urwid.disconnect_signal(widget, 'updatedescription', + self.updateDescription) + urwid.disconnect_signal(widget, 'updatetask', self.updateTask) def connectTaskListSignals(self, task_list): urwid.connect_signal(task_list, 'complete', self.completeTask) @@ -230,7 +301,8 @@ class CmdAsana: def connectDetailsSignals(self, task_details): urwid.connect_signal(task_details, 'comment', self.addComment) urwid.connect_signal(task_details, 'loadproject', self.showProject) - urwid.connect_signal(task_details, 'updatedescription', self.updateDetails) + urwid.connect_signal(task_details, 'updatedescription', + self.updateDescription) urwid.connect_signal(task_details, 'updatetask', self.updateTask) def handleInput(self, key): @@ -259,11 +331,11 @@ class CmdAsana: else: raise KeyError - loop = urwid.MainLoop(self.frame, + self.loop = urwid.MainLoop(self.frame, unhandled_input=self.handleInput, palette=ui.palette ) - loop.run() + self.loop.run() def main(): cmdasana = CmdAsana() From 1d462dbf33844a97f9f7d52cc302c31eaf108c2f Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 26 Jul 2015 17:00:08 -0700 Subject: [PATCH 3/5] Automatically update description when edit looses focus Closes #42682572598273 --- ui.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ui.py b/ui.py index 6346129..42b6ea2 100644 --- a/ui.py +++ b/ui.py @@ -236,8 +236,8 @@ class TaskDetails(urwid.Pile): comment_edit = CommentEdit(task) urwid.connect_signal(comment_edit, 'comment', self.comment) - description_edit = DescriptionEdit(task) - urwid.connect_signal(description_edit, 'updatedescription', + self.description_edit = DescriptionEdit(task) + urwid.connect_signal(self.description_edit, 'updatedescription', self.updateDescription) task_name_edit = TaskNameEdit(task) @@ -258,7 +258,7 @@ class TaskDetails(urwid.Pile): ('pack', task_name_edit), ('pack', assignee), ('pack', urwid.Divider('-')), - ('pack', description_edit), + ('pack', self.description_edit), ('pack', urwid.Divider('-')), ] + \ [('pack', urwid.Text('[' + story['created_by']['name'] + '] ' + \ @@ -269,6 +269,16 @@ class TaskDetails(urwid.Pile): super(TaskDetails, self).__init__(body) + def keypress(self, size, key): + key = super(TaskDetails, self).keypress(size, key) + + if self.focus != self.description_edit and \ + self.description_edit.edit_text != self.task['notes']: + self.updateDescription(self.task['id'], + self.description_edit.edit_text) + + return key + def comment(self, task_id, comment): urwid.emit_signal(self, 'comment', task_id, comment) From 7ed9f2f4b2024b7e6d6fbc6ea77bbb13c95037e5 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 26 Jul 2015 19:55:26 -0700 Subject: [PATCH 4/5] Able to assign tasks. Closes #42223443476020 --- cmdasana.py | 34 +++++++++++++++++++++++++---- ui.py | 61 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/cmdasana.py b/cmdasana.py index 9d0705a..e52d244 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -151,6 +151,13 @@ class CmdAsana: thread = Thread(target=runInThread) thread.start() + + def assignTask(self, task_id, user_id): + def runInThread(): + self.client.tasks.update(task_id, assignee=user_id) + + thread = Thread(target=runInThread) + thread.start() def addComment(self, task_id, comment): def runInThread(): @@ -160,6 +167,18 @@ class CmdAsana: thread = Thread(target=runInThread) thread.start() + def userTypeAhead(self, text, callback): + def runInThread(): + users = self.client.workspaces.typeahead(self.state['workspace_id'], + { + 'type': 'user', + 'query': text, + 'count': 5 + }) + callback(users) + thread = Thread(target=runInThread) + thread.start() + def replaceBody(self, widget): old_widget,_ = self.frame.contents.pop() if old_widget != None: @@ -271,18 +290,22 @@ class CmdAsana: 'loadproject', 'updatedescription', 'updatetask', + 'usertypeahead', + 'assigntask', + ]) + + urwid.register_signal(ui.AssigneeTypeAhead, [ + 'usertypeahead', + 'assigntask', ]) urwid.register_signal(ui.CommentEdit, ['comment']) - urwid.register_signal(ui.DescriptionEdit, ['updatedescription']) - urwid.register_signal(ui.TaskNameEdit, 'updatetask') - urwid.register_signal(ui.WorkspaceMenu, 'click') - urwid.register_signal(ui.ProjectList, 'loadproject') + def clearSignals(self, widget): urwid.disconnect_signal(widget, 'complete', self.completeTask) urwid.disconnect_signal(widget, 'newtask', self.newTask) @@ -291,6 +314,7 @@ class CmdAsana: urwid.disconnect_signal(widget, 'updatedescription', self.updateDescription) urwid.disconnect_signal(widget, 'updatetask', self.updateTask) + urwid.disconnect_signal(widget, 'usertypeahead', self.userTypeAhead) def connectTaskListSignals(self, task_list): urwid.connect_signal(task_list, 'complete', self.completeTask) @@ -304,6 +328,8 @@ class CmdAsana: urwid.connect_signal(task_details, 'updatedescription', self.updateDescription) urwid.connect_signal(task_details, 'updatetask', self.updateTask) + urwid.connect_signal(task_details, 'usertypeahead', self.userTypeAhead) + urwid.connect_signal(task_details, 'assigntask', self.assignTask) def handleInput(self, key): if key in ('q', 'Q'): diff --git a/ui.py b/ui.py index 42b6ea2..ee99142 100644 --- a/ui.py +++ b/ui.py @@ -49,6 +49,11 @@ class PagerButton(urwid.Button): super(PagerButton, self).__init__(('pager', 'load more')) urwid.connect_signal(self, 'click', loadPage) +class TypeAheadButton(urwid.Button): + def __init__(self, item, onClick): + super(TypeAheadButton, self).__init__(item['name']) + urwid.connect_signal(self, 'click', onClick, item) + class ProjectIcon(urwid.SelectableIcon): def __init__(self, project, onClick): self.project = project @@ -194,7 +199,7 @@ class TaskEdit(urwid.Edit): class CommentEdit(urwid.Edit): def __init__(self, task): self.task = task - super(CommentEdit, self).__init__(('secondary', 'Add a comment:\n')) + super(CommentEdit, self).__init__(('secondary', u'Add a comment:\n')) def keypress(self, size, key): if key != 'enter': @@ -205,7 +210,7 @@ class TaskNameEdit(urwid.Edit): def __init__(self, task): self.task = task super(TaskNameEdit, self).__init__(('secondary', - '#' + str(task['id']) + " "), + u'#' + str(task['id']) + ' '), task['name']) def keypress(self, size, key): @@ -218,7 +223,7 @@ class TaskNameEdit(urwid.Edit): class DescriptionEdit(urwid.Edit): def __init__(self, task): self.task = task - super(DescriptionEdit, self).__init__(('secondary', 'Description:\n'), + super(DescriptionEdit, self).__init__(('secondary', u'Description:\n'), task['notes'], multiline=True) @@ -228,6 +233,37 @@ class DescriptionEdit(urwid.Edit): urwid.emit_signal(self, 'updatedescription', self.task['id'], self.edit_text) +class AssigneeTypeAhead(urwid.Pile): + def __init__(self, task): + self.task = task + + if task['assignee'] != None: + assignee = task['assignee']['name'] + else: + assignee = "" + + self.edit = urwid.Edit('Assignee: ', assignee) + urwid.connect_signal(self.edit, 'change', self.typeAhead) + + body = [('pack', self.edit)] + + super(AssigneeTypeAhead, self).__init__(body) + + def typeAhead(self, widget, text): + urwid.emit_signal(self, 'usertypeahead', text, self.updateTypeAhead) + + def updateTypeAhead(self, users): + users = [(TypeAheadButton(u, self.assign), ('pack', None)) for u in users] + + users.insert(0, self.contents[0]) + + self.contents = users + + def assign(self, widget, user): + urwid.emit_signal(self, 'assigntask', self.task['id'], user['id']) + self.contents = [self.contents[0]] + self.edit.set_edit_text(user['name']) + class TaskDetails(urwid.Pile): def __init__(self, task, stories): self.task = task @@ -243,20 +279,19 @@ class TaskDetails(urwid.Pile): task_name_edit = TaskNameEdit(task) urwid.connect_signal(task_name_edit, 'updatetask', self.updateTask) + assignee_type_ahead = AssigneeTypeAhead(task) + urwid.connect_signal(assignee_type_ahead, 'usertypeahead', + self.userTypeAhead) + urwid.connect_signal(assignee_type_ahead, 'assigntask', self.assignTask) + projects = [('pack', ProjectIcon(project, self.loadProject)) for project in task['projects']] - if task['assignee']: - assignee = urwid.Text('Assigned to: ' + task['assignee']['name']) - else: - assignee = urwid.Text(('secondary', '(not assigned)')) - - body = projects + \ [ ('pack', urwid.Divider('=')), ('pack', task_name_edit), - ('pack', assignee), + ('pack', assignee_type_ahead), ('pack', urwid.Divider('-')), ('pack', self.description_edit), ('pack', urwid.Divider('-')), @@ -290,3 +325,9 @@ class TaskDetails(urwid.Pile): def loadProject(self, project_id): urwid.emit_signal(self, 'loadproject', project_id) + + def userTypeAhead(self, text, callback): + urwid.emit_signal(self, 'usertypeahead', text, callback) + + def assignTask(self, task_id, user_id): + urwid.emit_signal(self, 'assigntask', task_id, user_id) From eb11f2933bd944b020a032c643236ad33a354e73 Mon Sep 17 00:00:00 2001 From: Aaron Gutierrez Date: Sun, 26 Jul 2015 21:40:03 -0700 Subject: [PATCH 5/5] Redraw screen after loading typeahead --- cmdasana.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmdasana.py b/cmdasana.py index e52d244..632214f 100755 --- a/cmdasana.py +++ b/cmdasana.py @@ -176,6 +176,8 @@ class CmdAsana: 'count': 5 }) callback(users) + self.loop.draw_screen() + thread = Thread(target=runInThread) thread.start()