diff --git a/gtd.py b/gtd.py index 226bcb6..72863b0 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 aa52d39..e5dd5ad 100644 --- a/todo/card.py +++ b/todo/card.py @@ -10,11 +10,14 @@ import arrow import click import trello +import time + from prompt_toolkit import prompt from prompt_toolkit.validation import Validator 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 @@ -61,6 +64,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 +303,30 @@ def change_description(self): ) return new_desc + def change_checklists(self): + old_checklists = self.card_json['Checklists'] + + checklist_handling = ChecklistHandler(connection=self.connection, id=self.id, checklists=old_checklists) + checklists_to_edit = checklist_handling.parse_checklists() + 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() + + return new_checklists + def search_for_regex(card, title_regex, regex_flags): try: @@ -397,6 +432,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 = [] @@ -416,6 +453,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/checklist_handler.py b/todo/checklist_handler.py new file mode 100644 index 0000000..db162db --- /dev/null +++ b/todo/checklist_handler.py @@ -0,0 +1,132 @@ +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: + 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: + items = [] + for item in checklist.items: + if item['state'] == 'complete': + items.append("[x] " + item['name']) + elif item['state'] == 'incomplete': + 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): + 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 + except: + print("Unknown error!") + print("Opening editor again") + time.sleep(3) + return None, False + + new_checklists = [] + print("Adjusting checklists...") + for checklist in tqdm(checklist_objects): + cl = checklist.add_to_trello() + new_checklists.append(cl) + return new_checklists, True + + 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", " ") + + while cleaned_checklist_string != cleaned_checklist_string.replace("[]", "[ ]"): + cleaned_checklist_string = cleaned_checklist_string.replace("[]", "[ ]") + + 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): + print("Cleaning checklists...") + for old_checklist in tqdm(self.checklists): + old_checklist.delete() 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'])