From 70590a6cd8322189a3a5245fb9eaab0500afa922 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Sat, 20 Jun 2020 22:14:31 +0200 Subject: [PATCH 1/7] Adding checklists to be manipulated in vim --- gtd.py | 5 ++++ todo/card.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++ todo/connection.py | 1 + todo/display.py | 9 +++++++ 4 files changed, 80 insertions(+) diff --git a/gtd.py b/gtd.py index 6cb12b3..8300f3c 100755 --- a/gtd.py +++ b/gtd.py @@ -50,6 +50,8 @@ def __init__(self, config: Configuration): self.connection = TrelloConnection(config) self.display = Display(config, self.connection) self.board = self.connection.main_board() + self.checklists = self.board.get_checklists() + # Cached state for card_repl self._list_choices = build_name_lookup(self.connection.main_board().get_lists('open')) self._label_choices = build_name_lookup(self.connection.main_board().get_labels(limit=200)) @@ -88,6 +90,7 @@ def card_repl(self, card: dict) -> bool: 'delete': 'permanently delete this card', 'duedate': 'add a due date or change the due date', 'description': 'change the description of this card (desc)', + 'checklists': 'change the checklists of this card (check)', 'help': 'display this help output (h)', 'move': 'move to a different board and list (m)', 'next': 'move to the next card (n)', @@ -117,6 +120,8 @@ def card_repl(self, card: dict) -> bool: webbrowser.open(url) elif user_input in ['desc', 'description']: card.change_description() + elif user_input in ['check', 'checklists']: + card.change_checklists() elif user_input == 'delete': card.delete() print('Card deleted') diff --git a/todo/card.py b/todo/card.py index bd2ad76..5ef3f64 100644 --- a/todo/card.py +++ b/todo/card.py @@ -10,6 +10,7 @@ import arrow import click import trello + from prompt_toolkit import prompt from prompt_toolkit.validation import Validator from prompt_toolkit.completion import WordCompleter, FuzzyWordCompleter @@ -61,6 +62,14 @@ def id(self): def fetch(self): '''Refresh the base card JSON structure''' self.card_json = self.connection.trello.fetch_json('/cards/' + self.id, query_params={'fields': 'all'}) + checklists = self.connection.main_board().get_checklists() + + this_checklist = [] + for id in self.card_json['idChecklists']: + for item in checklists: + if id == item.id: + this_checklist.append(item) + self.card_json['Checklists'] = this_checklist def fetch_comments(self, force: bool = False): '''Fetch the comments on this card and return them in JSON format, adding them into self.card_json @@ -292,6 +301,54 @@ def change_description(self): ) return new_desc + def change_checklists(self): + old_checklists = self.card_json['Checklists'] + client = old_checklists[0].client + + checklists_to_edit = "" + for checklist in old_checklists: + checklists_to_edit += "{name}:\n".format( + name=checklist.name.replace("\n", " ") + ) + for item in checklist.items: + if item['state'] == 'complete': + checklists_to_edit += ' ' * 6 + "[x] " + item['name'] + "\n" + elif item['state'] == 'incomplete': + checklists_to_edit += ' ' * 6 + "[ ] " + item['name'] + "\n" + checklists_to_edit += "\n" + + new_checklists_edited = click.edit(text=checklists_to_edit) + + if new_checklists_edited.endswith("\n"): + new_checklists_edited = new_checklists_edited[:-1] + + new_checklists = [] + for checklist in new_checklists_edited.split("\n\n"): + splitted_lines = checklist.split(":\n") + name = splitted_lines[0].split(":")[0] + + json_obj = client.fetch_json( + '/cards/' + self.id + '/checklists', + http_method='POST', + post_args={'name': name}, ) + + cl = trello.checklist.Checklist(client, [], json_obj, trello_card=self.id) + + for line in splitted_lines[1].splitlines(): + line = line.lstrip() + line = line[1:].split("] ") + if line[0] == "x" or line[0] == "X": + cl.add_checklist_item(line[1], checked=True) + elif line[0] == " ": + cl.add_checklist_item(line[1], checked=False) + new_checklists.append(cl) + self.card_json['Checklists'] = new_checklists + + for old_checklist in old_checklists: + old_checklist.delete() + + return new_checklists + def search_for_regex(card, title_regex, regex_flags): try: @@ -395,6 +452,8 @@ def create(context, **kwargs): cards_json = context.connection.trello.fetch_json(f'/boards/{context.board.id}', query_params=query_params) target_cards.extend(cards_json['cards']) + checklists = context.board.get_checklists() + # Post-process the returned JSON, filtering down to the other passed parameters filters = [] post_processed_cards = [] @@ -414,6 +473,12 @@ def create(context, **kwargs): for card in target_cards: if all(filter_func(card) for filter_func in filters): + this_checklist = [] + for id in card['idChecklists']: + for item in checklists: + if id == item.id: + this_checklist.append(item) + card['Checklists'] = this_checklist post_processed_cards.append(card) if not post_processed_cards: diff --git a/todo/connection.py b/todo/connection.py index a511023..01c17cc 100644 --- a/todo/connection.py +++ b/todo/connection.py @@ -62,6 +62,7 @@ def main_board(self): board_json = self.boards[0] board_object = trello.Board.from_json(self.trello, json_obj=board_json) self._main_board = board_object + return board_object def boards_by_name(self): diff --git a/todo/display.py b/todo/display.py index 6650eff..ebbe81c 100644 --- a/todo/display.py +++ b/todo/display.py @@ -154,3 +154,12 @@ def show_card(self, card: dict): indent_print('Description', '') for line in card['desc'].splitlines(): print(' ' * 4 + line) + + indent_print('Checklists', '') + for checklist in card['Checklists']: + print(' ' * 4 + checklist.name + ":") + for item in checklist.items: + if item['state'] == 'complete': + print(' ' * 6 + "[x] " + item['name']) + elif item['state'] == 'incomplete': + print(' ' * 6 + "[ ] " + item['name']) From 096a7c10e8c553646f6c5b74cb0010baf6e538f3 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Sat, 20 Jun 2020 22:37:55 +0200 Subject: [PATCH 2/7] improving checklist handling --- todo/card.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/todo/card.py b/todo/card.py index 5ef3f64..d0a4b9d 100644 --- a/todo/card.py +++ b/todo/card.py @@ -318,6 +318,12 @@ def change_checklists(self): checklists_to_edit += "\n" new_checklists_edited = click.edit(text=checklists_to_edit) + if new_checklists_edited == checklists_to_edit: + # no change done + return + elif new_checklists_edited is None: + # no save in editor + return if new_checklists_edited.endswith("\n"): new_checklists_edited = new_checklists_edited[:-1] @@ -342,6 +348,7 @@ def change_checklists(self): elif line[0] == " ": cl.add_checklist_item(line[1], checked=False) new_checklists.append(cl) + self.card_json['Checklists'] = new_checklists for old_checklist in old_checklists: From c151abd7f22e059632e364cf6ec602d23a55f42c Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Fri, 5 Feb 2021 20:54:34 +0100 Subject: [PATCH 3/7] bugfix for adding checklists --- todo/card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todo/card.py b/todo/card.py index d0a4b9d..4699f3c 100644 --- a/todo/card.py +++ b/todo/card.py @@ -347,7 +347,7 @@ def change_checklists(self): cl.add_checklist_item(line[1], checked=True) elif line[0] == " ": cl.add_checklist_item(line[1], checked=False) - new_checklists.append(cl) + new_checklists.append(cl) self.card_json['Checklists'] = new_checklists From 4a5a72f77d157443f2b5478baa5f587cdd3d0429 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Fri, 5 Feb 2021 23:06:06 +0100 Subject: [PATCH 4/7] move checklist handling in separate class --- todo/card.py | 45 +++-------------------- todo/checklist_handler.py | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 39 deletions(-) create mode 100644 todo/checklist_handler.py diff --git a/todo/card.py b/todo/card.py index a847768..1df33eb 100644 --- a/todo/card.py +++ b/todo/card.py @@ -16,6 +16,7 @@ from prompt_toolkit.completion import WordCompleter, FuzzyWordCompleter from todo.connection import TrelloConnection +from todo.checklist_handler import ChecklistHandler from todo.exceptions import GTDException from todo.input import prompt_for_confirmation, single_select from todo.misc import get_title_of_webpage, DevNullRedirect, VALID_URL_REGEX, return_on_eof, build_name_lookup @@ -303,21 +304,12 @@ def change_description(self): def change_checklists(self): old_checklists = self.card_json['Checklists'] - client = old_checklists[0].client - checklists_to_edit = "" - for checklist in old_checklists: - checklists_to_edit += "{name}:\n".format( - name=checklist.name.replace("\n", " ") - ) - for item in checklist.items: - if item['state'] == 'complete': - checklists_to_edit += ' ' * 6 + "[x] " + item['name'] + "\n" - elif item['state'] == 'incomplete': - checklists_to_edit += ' ' * 6 + "[ ] " + item['name'] + "\n" - checklists_to_edit += "\n" + checklist_handling = ChecklistHandler(connection=self.connection, id=self.id, checklists=old_checklists) + checklists_to_edit = checklist_handling.parse_checklists() new_checklists_edited = click.edit(text=checklists_to_edit) + if new_checklists_edited == checklists_to_edit: # no change done return @@ -325,34 +317,9 @@ def change_checklists(self): # no save in editor return - if new_checklists_edited.endswith("\n"): - new_checklists_edited = new_checklists_edited[:-1] - - new_checklists = [] - for checklist in new_checklists_edited.split("\n\n"): - splitted_lines = checklist.split(":\n") - name = splitted_lines[0].split(":")[0] - - json_obj = client.fetch_json( - '/cards/' + self.id + '/checklists', - http_method='POST', - post_args={'name': name}, ) - - cl = trello.checklist.Checklist(client, [], json_obj, trello_card=self.id) - - for line in splitted_lines[1].splitlines(): - line = line.lstrip() - line = line[1:].split("] ") - if line[0] == "x" or line[0] == "X": - cl.add_checklist_item(line[1], checked=True) - elif line[0] == " ": - cl.add_checklist_item(line[1], checked=False) - new_checklists.append(cl) - + new_checklists = checklist_handling.parse_edited_checklists(new_checklists_edited=new_checklists_edited) self.card_json['Checklists'] = new_checklists - - for old_checklist in old_checklists: - old_checklist.delete() + checklist_handling.remove_old_checklists() return new_checklists diff --git a/todo/checklist_handler.py b/todo/checklist_handler.py new file mode 100644 index 0000000..b47ae96 --- /dev/null +++ b/todo/checklist_handler.py @@ -0,0 +1,77 @@ +from todo.connection import TrelloConnection +import trello +import re + + +class ChecklistHandler: + def __init__(self, connection: TrelloConnection, id, checklists): + self.connection = connection + self.id = id + self.checklists = checklists + self.new_checklist = None + + def parse_checklists(self): + checklists_to_edit = "" + for checklist in self.checklists: + checklists_to_edit += "{name}:\n".format( + name=checklist.name.replace("\n", " ")) + for item in checklist.items: + if item['state'] == 'complete': + checklists_to_edit += ' ' * 6 + "[x] " + item['name'] + "\n" + elif item['state'] == 'incomplete': + checklists_to_edit += ' ' * 6 + "[ ] " + item['name'] + "\n" + checklists_to_edit += "\n" + return checklists_to_edit + + def parse_edited_checklists(self, new_checklists_edited): + cleaned_checklist_string = self.clean_checklist_string(new_checklists_edited) + + new_checklists = [] + for checklist in cleaned_checklist_string.split("\n\n"): + splitted_lines = checklist.split(":\n") + name = splitted_lines[0].split(":")[0] + + json_obj = self.connection.trello.fetch_json( + '/cards/' + self.id + '/checklists', + http_method='POST', + post_args={'name': name}, ) + + cl = trello.checklist.Checklist(self.connection.trello, [], json_obj, trello_card=self.id) + + for line in splitted_lines[1].splitlines(): + line = line.lstrip() + line = line[1:].split("] ") + if line[0] == "x" or line[0] == "X": + cl.add_checklist_item(line[1], checked=True) + elif line[0] == " ": + cl.add_checklist_item(line[1], checked=False) + new_checklists.append(cl) + return new_checklists + + + def clean_checklist_string(self, checklist_string): + cleaned_checklist_string = checklist_string + + while cleaned_checklist_string.endswith("\n"): + cleaned_checklist_string = cleaned_checklist_string[:-1] + + while cleaned_checklist_string.startswith("\n"): + cleaned_checklist_string = cleaned_checklist_string[1:] + + while cleaned_checklist_string != cleaned_checklist_string.replace("\n\n\n", "\n\n"): + cleaned_checklist_string = cleaned_checklist_string.replace("\n\n\n", "\n\n") + + while cleaned_checklist_string != cleaned_checklist_string.replace("]\n\n", "]\n"): + cleaned_checklist_string = cleaned_checklist_string.replace("]\n\n", "]\n") + + while cleaned_checklist_string != cleaned_checklist_string.replace("\t", " "): + cleaned_checklist_string = cleaned_checklist_string.replace("\t", " ") + + cleaned_checklist_string = re.sub(r"\n\n(\s+)\[", "\n [", cleaned_checklist_string) + + return cleaned_checklist_string + + + def remove_old_checklists(self): + for old_checklist in self.checklists: + old_checklist.delete() From f77832f6b8a76e6a6cd3c13ce0a809b91638f703 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Fri, 5 Feb 2021 23:52:26 +0100 Subject: [PATCH 5/7] add further checks for parsing --- todo/checklist_handler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/todo/checklist_handler.py b/todo/checklist_handler.py index b47ae96..a3fb320 100644 --- a/todo/checklist_handler.py +++ b/todo/checklist_handler.py @@ -67,7 +67,12 @@ def clean_checklist_string(self, checklist_string): while cleaned_checklist_string != cleaned_checklist_string.replace("\t", " "): cleaned_checklist_string = cleaned_checklist_string.replace("\t", " ") + while cleaned_checklist_string != cleaned_checklist_string.replace("[]", "[ ]"): + cleaned_checklist_string = cleaned_checklist_string.replace("[]", "[ ]") + cleaned_checklist_string = re.sub(r"\n\n(\s+)\[", "\n [", cleaned_checklist_string) + cleaned_checklist_string = re.sub(r"](\s+)(\w)", r"] \2", cleaned_checklist_string) + cleaned_checklist_string = re.sub(r"\[(\w)\](\w)", r"[\1]] \2", cleaned_checklist_string) return cleaned_checklist_string From 3ecdfa2b72d9897cadde3693d57fe574437320a1 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Sat, 6 Feb 2021 14:41:12 +0100 Subject: [PATCH 6/7] make change to yaml api for checklist IO --- todo/card.py | 26 ++++++---- todo/checklist_handler.py | 103 +++++++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/todo/card.py b/todo/card.py index 1df33eb..e5dd5ad 100644 --- a/todo/card.py +++ b/todo/card.py @@ -10,6 +10,7 @@ import arrow import click import trello +import time from prompt_toolkit import prompt from prompt_toolkit.validation import Validator @@ -307,17 +308,20 @@ def change_checklists(self): checklist_handling = ChecklistHandler(connection=self.connection, id=self.id, checklists=old_checklists) checklists_to_edit = checklist_handling.parse_checklists() - - new_checklists_edited = click.edit(text=checklists_to_edit) - - if new_checklists_edited == checklists_to_edit: - # no change done - return - elif new_checklists_edited is None: - # no save in editor - return - - new_checklists = checklist_handling.parse_edited_checklists(new_checklists_edited=new_checklists_edited) + success = False + editor_content = checklists_to_edit + while not success: + new_checklists_edited = click.edit(text=editor_content) + + if new_checklists_edited == checklists_to_edit: + # no change done + return + elif new_checklists_edited is None: + # no save in editor + # discard changes if editing failed + return + new_checklists, success = checklist_handling.parse_edited_checklists(new_checklists_edited=new_checklists_edited) + editor_content = new_checklists_edited self.card_json['Checklists'] = new_checklists checklist_handling.remove_old_checklists() diff --git a/todo/checklist_handler.py b/todo/checklist_handler.py index a3fb320..0760658 100644 --- a/todo/checklist_handler.py +++ b/todo/checklist_handler.py @@ -1,6 +1,28 @@ from todo.connection import TrelloConnection +import yaml import trello +import time import re +from tqdm import tqdm + + +class Checklist(object): + def __init__(self, id, connection, name, items, checks): + self.id = id + self.connection = connection + self.name = name + self.items = items + self.checks = checks + + def add_to_trello(self): + json_obj = self.connection.trello.fetch_json( + '/cards/' + self.id + '/checklists', + http_method='POST', + post_args={'name': self.name}, ) + cl = trello.checklist.Checklist(self.connection.trello, [], json_obj, trello_card=self.id) + for text, checked in zip(self.items, self.checks): + cl.add_checklist_item(text, checked=checked) + return cl class ChecklistHandler: @@ -13,41 +35,63 @@ def __init__(self, connection: TrelloConnection, id, checklists): def parse_checklists(self): checklists_to_edit = "" for checklist in self.checklists: - checklists_to_edit += "{name}:\n".format( - name=checklist.name.replace("\n", " ")) + items = [] for item in checklist.items: if item['state'] == 'complete': - checklists_to_edit += ' ' * 6 + "[x] " + item['name'] + "\n" + items.append("[x] " + item['name']) elif item['state'] == 'incomplete': - checklists_to_edit += ' ' * 6 + "[ ] " + item['name'] + "\n" - checklists_to_edit += "\n" + items.append("[ ] " + item['name']) + yaml_checklist = {checklist.name.replace("\n", ""): items} + checklists_to_edit += yaml.dump(yaml_checklist, default_flow_style=False) + "\n" return checklists_to_edit + + def parse_item_string(self, line): + line = line.lstrip() + line = line[1:].split("] ") + if line[0] == "x" or line[0] == "X": + return line[1], True + elif line[0] == " ": + return line[1], False + + def create_objects_from_list(self, l_checklists): + checklist_objects = [] + for checklist in l_checklists: + name = list(checklist.keys())[0] + checklist_items = list(checklist.values())[0] + checks = [] + texts = [] + for item in checklist_items: + text, checked = self.parse_item_string(item) + checks.append(checked) + texts.append(text) + this_checklist = Checklist(id=self.id, connection=self.connection, name=name, items=texts, checks=checks) + checklist_objects.append(this_checklist) + return checklist_objects + def parse_edited_checklists(self, new_checklists_edited): - cleaned_checklist_string = self.clean_checklist_string(new_checklists_edited) + try: + cleaned_checklist_string = self.clean_checklist_string(new_checklists_edited) + l_checklists = [yaml.safe_load(item) for item in cleaned_checklist_string.split("\n\n")] + checklist_objects = self.create_objects_from_list(l_checklists=l_checklists) + print("Parsing successful!") + except AttributeError: + print("Cannot parse the checklists from editor! YAML file not valid!") + print("Opening editor again") + time.sleep(3) + return None, False + except yaml.scanner.ScannerError: + print("ScannerError! YAML file not valid!") + print("Opening editor again") + time.sleep(3) + return None, False new_checklists = [] - for checklist in cleaned_checklist_string.split("\n\n"): - splitted_lines = checklist.split(":\n") - name = splitted_lines[0].split(":")[0] - - json_obj = self.connection.trello.fetch_json( - '/cards/' + self.id + '/checklists', - http_method='POST', - post_args={'name': name}, ) - - cl = trello.checklist.Checklist(self.connection.trello, [], json_obj, trello_card=self.id) - - for line in splitted_lines[1].splitlines(): - line = line.lstrip() - line = line[1:].split("] ") - if line[0] == "x" or line[0] == "X": - cl.add_checklist_item(line[1], checked=True) - elif line[0] == " ": - cl.add_checklist_item(line[1], checked=False) + print("Adjusting checklists...") + for checklist in tqdm(checklist_objects): + cl = checklist.add_to_trello() new_checklists.append(cl) - return new_checklists - + return new_checklists, True def clean_checklist_string(self, checklist_string): cleaned_checklist_string = checklist_string @@ -70,13 +114,14 @@ def clean_checklist_string(self, checklist_string): while cleaned_checklist_string != cleaned_checklist_string.replace("[]", "[ ]"): cleaned_checklist_string = cleaned_checklist_string.replace("[]", "[ ]") - cleaned_checklist_string = re.sub(r"\n\n(\s+)\[", "\n [", cleaned_checklist_string) + cleaned_checklist_string = re.sub(r"\n\n(\s+)\[", r"\n [", cleaned_checklist_string) cleaned_checklist_string = re.sub(r"](\s+)(\w)", r"] \2", cleaned_checklist_string) cleaned_checklist_string = re.sub(r"\[(\w)\](\w)", r"[\1]] \2", cleaned_checklist_string) + cleaned_checklist_string = re.sub(r"\n\n-", r"\n-", cleaned_checklist_string) return cleaned_checklist_string - def remove_old_checklists(self): - for old_checklist in self.checklists: + print("Cleaning checklists...") + for old_checklist in tqdm(self.checklists): old_checklist.delete() From 429ad8e9091254a10069158ba77ae9fe2b971141 Mon Sep 17 00:00:00 2001 From: Tim Michael Heinz Wolf Date: Sat, 6 Feb 2021 15:03:44 +0100 Subject: [PATCH 7/7] add unknown error --- todo/checklist_handler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/todo/checklist_handler.py b/todo/checklist_handler.py index 0760658..db162db 100644 --- a/todo/checklist_handler.py +++ b/todo/checklist_handler.py @@ -85,6 +85,11 @@ def parse_edited_checklists(self, new_checklists_edited): print("Opening editor again") time.sleep(3) return None, False + except: + print("Unknown error!") + print("Opening editor again") + time.sleep(3) + return None, False new_checklists = [] print("Adjusting checklists...")