From 9b3aae9ab389cc9f967640fd451a045c7d9ac34e Mon Sep 17 00:00:00 2001 From: David Baldwynn Date: Mon, 26 Mar 2018 11:56:32 -0700 Subject: [PATCH 1/3] added assigned and completed flags, and show comments --- .gitignore | 1 + gtd.py | 89 ++++++++++++++++++++++++++++++++++++++++++++---- todo/__init__.py | 2 +- todo/display.py | 69 ++++++++++++++++++++++++++++++++++++- todo/input.py | 27 ++++++++++++++- 5 files changed, 178 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 4f06c6e..291627c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ share/* build/* dist/* *egg-info* +.env \ No newline at end of file diff --git a/gtd.py b/gtd.py index 5f2b27e..bdf3f49 100755 --- a/gtd.py +++ b/gtd.py @@ -9,6 +9,7 @@ import requests import readline # noqa import webbrowser +from datetime import datetime from requests_oauthlib import OAuth1Session from requests_oauthlib.oauth1_session import TokenRequestDenied from todo.input import prompt_for_confirmation, BoardTool, CardTool @@ -29,6 +30,9 @@ def filtering_command(f): f = click.option('-l', '--listname', help='Only show cards from this list', default=None)(f) f = click.option('--attachments', is_flag=True, help='Only show cards which have attachments', default=None)(f) f = click.option('--has-due', is_flag=True, help='Only show cards which have due dates', default=None)(f) + f = click.option('-a', '--assigned', is_flag=True, help='Only show cards assigned to you')(f) + f = click.option('-c', '--completed', is_flag=True, help='Only show cards assigned and completed by you')(f) + f = click.option('--include_closed', is_flag=True, help='Include archived cards', default=None)(f) return f @@ -209,7 +213,7 @@ def show(): @pass_config def show_lists(config, json, show_all): '''Display all lists on this board''' - _, board = BoardTool.start(config) + _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() @@ -218,12 +222,79 @@ def show_lists(config, json, show_all): display.show_raw(list_names, use_json=json) +def get_mentions_from_text(text): + trello_username_re = re.compile(r'@([A-Za-z0-9_]+)') + return trello_username_re.findall(text) + +def has_replied_to_comment(connection, original_comment_text, creator_username, mentioned_username, card_id): + current_card = connection.trello.get_card(card_id) + comments = current_card.fetch_comments(force=True) + has_response = False + selected_comment_i = None + + for i, comment in enumerate(comments): + # If creator of previous comment is mentoined in comment text and + # previously mentioned username is creator of this comment, we have + # found reply to original comment + comment_text = comment["data"]["text"] + usernames_in_comment = get_mentions_from_text(comment["data"]["text"]) + + if mentioned_username in usernames_in_comment: + has_response = True + if original_comment_text == comment_text: + selected_comment_i = i + break + + if selected_comment_i is not None and has_response is True: + return True + return False + +def get_unresponded_comments(connection, current_user): + notifications = connection.trello.fetch_json('/members/me/notifications', + query_params={'filter': ",".join(['mentionedOnCard'])}) + + unresponded_comments = [] + for notif in notifications: + comment_creator = notif["memberCreator"] + comment_text = notif["data"]["text"] + comment_card = notif["data"]["card"] + comment_board = notif["data"]["board"] + comment_date = datetime.strptime(notif["date"], "%Y-%m-%dT%H:%M:%S.%fZ") + + if not has_replied_to_comment(connection, comment_text, current_user.username, comment_creator['username'], comment_card['id']): + unresponded_comments.append({ + "creator": comment_creator, + "text": comment_text, + "card": comment_card, + "board": comment_board, + "date": comment_date, + "id": notif["id"] + }) + + date_str = "{:%b %d, %Y}".format(comment_date) + + return unresponded_comments + + +@show.command('unresponded') +@json_option +@pass_config +def show_unresponded(config, json): + '''Display all lists on this board''' + connection, _, current_user = BoardTool.start(config) + display = Display(config.color) + if config.banner and not json: + display.banner() + comments = get_unresponded_comments(connection, current_user) + display.show_comments(comments, use_json=json) + + @show.command('tags') @json_option @pass_config def show_tags(config, json): '''Display all tags on this board''' - _, board = BoardTool.start(config) + _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() @@ -236,14 +307,14 @@ def show_tags(config, json): @json_option @click.option('--tsv', is_flag=True, default=False, help='Output as tab-separated values') @pass_config -def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, has_due): +def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, has_due, assigned, completed, include_closed): '''Display cards The show command prints a table of all the cards with fields that will fit on the terminal you're using. You can change this formatting by passing one of --tsv or --json, which will output as a tab-separated value sheet or JSON. This command along with the batch & review commands share a flexible argument scheme for getting card information. Mutually exclusive arguments include -t/--tags & --no-tags along with -j/--json & --tsv ''' - _, board = BoardTool.start(config) + _, board, current_user = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() @@ -251,10 +322,14 @@ def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, h board, tags=tags, no_tags=no_tags, + current_user=current_user, title_regex=match, list_regex=listname, has_attachments=attachments, - has_due_date=has_due + has_due_date=has_due, + assigned=assigned, + completed=completed, + include_closed=include_closed ) display.show_cards(cards, use_json=json, tsv=tsv) @@ -276,7 +351,7 @@ def delete(): def delete_lists(config, name, noninteractive): '''Delete lists containing the substring ''' - _, board = BoardTool.start(config) + _, board, _ = BoardTool.start(config) lists = [l for l in board.get_lists('open') if name in l.name] if noninteractive: [l.set_closed() for l in lists] @@ -295,7 +370,7 @@ def delete_lists(config, name, noninteractive): def delete_cards(config, force, noninteractive, tags, no_tags, match, listname, attachments, has_due): '''Delete a set of cards specified ''' - _, board = BoardTool.start(config) + _, board, _ = BoardTool.start(config) display = Display(config.color) if config.banner and not json: display.banner() diff --git a/todo/__init__.py b/todo/__init__.py index 5f6a066..ae3680d 100644 --- a/todo/__init__.py +++ b/todo/__init__.py @@ -1,3 +1,3 @@ '''gtd.py''' -__version__ = '0.6.12' +__version__ = '0.7.12' __author__ = 'delucks' diff --git a/todo/display.py b/todo/display.py index 53963fb..6dd7e41 100644 --- a/todo/display.py +++ b/todo/display.py @@ -134,13 +134,80 @@ def show_cards(self, cards, use_json=False, tsv=False, table_fields=[], field_bl else: print(self.resize_and_get_table(table, fields.keys())) + def show_comments(self, comments, use_json=False, tsv=False, table_fields=[], field_blacklist=[]): + '''Display an iterable of cards all at once. + Uses a pretty-printed table by default, but can also print JSON and tab-separated values (TSV). + Supports the following cli commands: + show unresponded + + :param list(trello.Card)|iterable(trello.Card) cards: cards to show + :param bool use_json: display all metadata of these cards in JSON format + :param bool tsv: display these cards using a tab-separated value format + :param list table_fields: display only these fields (overrides field_blacklist) + :param list field_blacklist: display all except these fields + ''' + if use_json: + sanitized_comments = list(map( + lambda d: d.pop('client') and d, + [c.__dict__.copy() for c in comments] + )) + tostr = self._force_json(sanitized_comments) + print(json.dumps(tostr, sort_keys=True, indent=2)) + else: + # TODO implement a custom sorting functions so the table can be sorted by multiple columns + fields = OrderedDict() + # This is done repetitively to establish column order + fields['comment'] = lambda c: c['text'] + fields['card'] = lambda c: c['card']['name'] + fields['creator'] = lambda c: '@' + c['creator']['username'] + fields['last activity'] = lambda c: c['date'] + fields['board'] = lambda c: c['board']['name'] + fields['id'] = lambda c: c['id'] + fields['url'] = lambda c: 'trello.com/c/' + c['card']['shortLink'] + table = prettytable.PrettyTable() + table.field_names = fields.keys() + table.align = 'l' + if tsv: + table.set_style(prettytable.PLAIN_COLUMNS) + else: + table.hrules = prettytable.FRAME + with click.progressbar(list(comments), label='Fetching comments', width=0) as pg: + for comment in pg: + table.add_row([x(comment) for x in fields.values()]) + try: + table[0] + except IndexError: + click.secho('No comments match!', fg='red') + raise GTDException(1) + if table_fields: + print(self.resize_and_get_comment_table(table, table_fields)) + elif field_blacklist: + f = set(fields.keys()) - set(field_blacklist) + print(self.resize_and_get_comment_table(table, list(f))) + else: + print(self.resize_and_get_comment_table(table, fields.keys())) + + def resize_and_get_comment_table(self, table, fields): + '''Remove columns from the table until it fits in your terminal''' + maxwidth = click.get_terminal_size()[0] + possible = table.get_string(fields=fields, sortby='last activity') + fset = set(fields) + # Fields in increasing order of importance + to_remove = ['board', 'card', 'last activity', 'id', 'url'] + # Wait until we're under max width or until we can't discard more fields + while len(possible.splitlines()[0]) >= maxwidth and to_remove: + # Remove a field one at a time + fset.remove(to_remove.pop(0)) + possible = table.get_string(fields=list(fset)) + return possible + def resize_and_get_table(self, table, fields): '''Remove columns from the table until it fits in your terminal''' maxwidth = click.get_terminal_size()[0] possible = table.get_string(fields=fields, sortby='last activity') fset = set(fields) # Fields in increasing order of importance - to_remove = ['desc', 'id', 'board', 'url', 'last activity', 'list'] + to_remove = ['tags', 'due', 'desc', 'id', 'board', 'last activity', 'list'] # Wait until we're under max width or until we can't discard more fields while len(possible.splitlines()[0]) >= maxwidth and to_remove: # Remove a field one at a time diff --git a/todo/input.py b/todo/input.py index b5fce86..34f4008 100644 --- a/todo/input.py +++ b/todo/input.py @@ -358,7 +358,8 @@ class BoardTool: def start(config): connection = TrelloConnection(config) board = BoardTool.get_main_board(connection, config) - return connection, board + current_user = BoardTool.get_current_user(connection, config) + return connection, board, current_user @staticmethod def take_cards_from_lists(board, list_regex): @@ -389,12 +390,29 @@ def search_for_regex(card): has_attachments = kwargs.get('has_attachments', None) no_tags = kwargs.get('no_tags', False) has_due_date = kwargs.get('has_due_date', None) + # flag that sets whether to filter by assigned cards or not + assigned = kwargs.get('assigned', False) + # Value of current user. Used in assigned and completed filter + current_user = kwargs.get('current_user', None) + + #flag that shows archived and non-archived cards + include_closed = kwargs.get('is_closed', False) + + #flag that shows only completed cards for user + completed = kwargs.get('completed', False) + # comma-separated string of tags to filter on tags = kwargs.get('tags', None) # custom user-supplied callable functions to filter a card on filter_funcs = kwargs.get('filter_funcs', None) # Parse arguments into callables filters = [] + if completed: + filters.append(lambda c: current_user.id in c.member_id and c.get_list().name.lower() == 'done') + if assigned: + filters.append(lambda c: current_user.id in c.member_id) + if not include_closed: + filters.append(lambda c: not c.closed) if tags: filters.append(partial(tags_on_card, tags=tags)) if no_tags: @@ -438,6 +456,11 @@ def get_main_board(connection, config): else: return connection.trello.list_boards('open')[0] + @staticmethod + def get_current_user(connection, config): + '''use the configuration to get the main board & return it''' + return connection.trello.get_member('me') + @staticmethod def get_inbox_list(connection, config): '''use the configuration to get the main board & list from @@ -456,3 +479,5 @@ def list_lookup(board): @staticmethod def label_lookup(board): return {o.name: o for o in board.get_labels()} + + From 8bc1c295a77c8b4d7c437095adab5c66f8acc9fc Mon Sep 17 00:00:00 2001 From: David Baldwynn Date: Mon, 26 Mar 2018 12:28:38 -0700 Subject: [PATCH 2/3] fixed help text for show unresponded --- README.rst | 5 +++++ gtd.py | 6 +++--- setup.py | 12 ++++++------ todo/__init__.py | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index b7691eb..5f25da4 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,11 @@ The project is named "gtd.py" because it was initially built as a tool for me to Usage ----- +Displaying Unresponded Comments +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``show unresponded`` command will return all comments that mention your username that you have not responded to yet. + Displaying Cards ^^^^^^^^^^^^^^^^ diff --git a/gtd.py b/gtd.py index bdf3f49..f861cae 100755 --- a/gtd.py +++ b/gtd.py @@ -201,9 +201,9 @@ def onboard(no_open, output_path=None): # show {{{ -@cli.group(short_help='Display cards, tags, or lists on this board') +@cli.group(short_help='Display cards, tags, comments or lists on this board') def show(): - '''Display cards, tags, or lists on this board.''' + '''Display cards, tags, comments or lists on this board.''' pass @@ -280,7 +280,7 @@ def get_unresponded_comments(connection, current_user): @json_option @pass_config def show_unresponded(config, json): - '''Display all lists on this board''' + '''Display all unresponded comments for current account''' connection, _, current_user = BoardTool.start(config) display = Display(config.color) if config.banner and not json: diff --git a/setup.py b/setup.py index 2bac406..d5730c3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ readme_contents = f.read() setup( - name='gtd.py', + name='mello', description='A Fast Command-line Interface for Trello', long_description=readme_contents, version=__version__, @@ -20,15 +20,15 @@ py_modules=['gtd'], entry_points={ 'console_scripts': [ - 'gtd = gtd:main' + 'mello = gtd:main' ] }, python_requires='>=3', - author = 'James Luck', - author_email = 'me@jamesluck.com', + author = 'David Baldwynn', + author_email = 'david@countable.ca', license='BSD 3-clause', - url = 'https://github.com/delucks/gtd.py', - download_url = 'https://github.com/delucks/gtd.py/tarball/{}'.format(__version__), + url = 'https://github.com/whitef0x0/gtd.py', + download_url = 'https://github.com/whitef0x0/gtd.py/tarball/{}'.format(__version__), keywords = ['productivity', 'cli', 'trello', 'gtd', 'getting things done'], classifiers = [ 'Development Status :: 4 - Beta', diff --git a/todo/__init__.py b/todo/__init__.py index ae3680d..e107160 100644 --- a/todo/__init__.py +++ b/todo/__init__.py @@ -1,3 +1,3 @@ '''gtd.py''' -__version__ = '0.7.12' -__author__ = 'delucks' +__version__ = '0.7.13' +__author__ = 'whitef0x0' From c7d45c66fdefa6a6bca6c738c2b3be4589c1a00e Mon Sep 17 00:00:00 2001 From: David Baldwynn Date: Mon, 26 Mar 2018 12:44:17 -0700 Subject: [PATCH 3/3] added installation section to README --- README.rst | 76 ++++++++++++++++++++++++++-------------------- gtd.py => mello.py | 0 setup.py | 4 +-- todo/__init__.py | 2 +- 4 files changed, 46 insertions(+), 36 deletions(-) rename gtd.py => mello.py (100%) diff --git a/README.rst b/README.rst index 5f25da4..2082588 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -gtd.py +Mello ======= A Fast Command-line Interface for Trello @@ -6,7 +6,17 @@ A Fast Command-line Interface for Trello This is a command-line tool that enables you to add, sort, and review cards on Trello rapidly. It is designed to reduce the amount of friction between your thoughts and your TODO list, especially if you never leave the terminal. -The project is named "gtd.py" because it was initially built as a tool for me to maintain a Trello board using the GTD_ task tracking method. I've been actively using this tool for GTD since the first commit; if you're trying to use GTD with Trello this is the tool for you. +Installation +------------ + +When installing, make sure to use pip3 if you your machine defaults to python2.7 +1. Install via pip +`pip3 install mello` + +1.1 (Optional) Add python3 bin to PATH if you haven't already done so + +2. Setup OAuth credentials +`mello reconfigure` Usage @@ -25,24 +35,24 @@ The ``show`` command will return all the cards which match your supplied argumen :: # Show cards from the list "Inbox" matching a regular expression on their titles - $ gtd show cards -l Inbox -m 'https?' + $ mello show cards -l Inbox -m 'https?' # Show cards which have no tags but have due dates, in pretty-printed JSON format - $ gtd show cards --no-tags --has-due -j + $ mello show cards --no-tags --has-due -j -``grep`` faithfully implements some flags from the venerable utility, including -c, -i, and -e! An invocation of this command is similar to a longer invocation of ``show``: ``gtd grep 'some_pattern'`` is equivalent to ``gtd show cards -m 'some_pattern'``. +``grep`` faithfully implements some flags from the venerable utility, including -c, -i, and -e! An invocation of this command is similar to a longer invocation of ``show``: ``mello grep 'some_pattern'`` is equivalent to ``mello show cards -m 'some_pattern'``. :: # Filter all cards based on a regex - $ gtd grep 'http.*amazon' + $ mello grep 'http.*amazon' # or multiple regexes! - $ gtd grep -e '[Jj]ob' -e 'career' -e '[oO]pportunity?' + $ mello grep -e '[Jj]ob' -e 'career' -e '[oO]pportunity?' # Use other popular grep flags! - $ gtd grep -ci 'meeting' + $ mello grep -ci 'meeting' Creating Things ^^^^^^^^^^^^^^^^ @@ -58,13 +68,13 @@ The command you'll probably use most frequently is ``add card``. Here are some c :: # Add a new card with title "foo" - $ gtd add card foo + $ mello add card foo # Specify a description with the card title - $ gtd add card foo -m 'Description for my new card' + $ mello add card foo -m 'Description for my new card' # Open $EDITOR so you can write the card title - $ gtd add card + $ mello add card The other subcommands for ``add`` (``add list`` and ``add tag``) are self-explanatory. @@ -76,13 +86,13 @@ The ``delete`` subcommand allows you to get rid of lists & cards. By default, ca :: # Archive all cards whose titles match this regular expression - $ gtd delete cards -m 'on T(hurs|ues)day' + $ mello delete cards -m 'on T(hurs|ues)day' # Delete without intervention all cards containing the string "testblah" - $ gtd delete cards --noninteractive --force -m 'testblah' + $ mello delete cards --noninteractive --force -m 'testblah' # Delete the list named "Temporary work" - $ gtd delete list "Temporary work" + $ mello delete list "Temporary work" Manipulating Cards in Bulk @@ -93,35 +103,35 @@ Frequently it's useful to move a whole bunch of cards at once, tag cards that ma :: # Tag all cards that have no tags - $ gtd batch tag --no-tags + $ mello batch tag --no-tags # Find all cards with a URL in their title and move those URLs into their attachments - $ gtd batch attach + $ mello batch attach # Move all cards in your "Inbox" list - $ gtd batch move -l Inbox + $ mello batch move -l Inbox # Set the due dates for all cards in a list containing the substring "Week" - $ gtd batch due -l Week + $ mello batch due -l Week # Change the due date for all cards that have one already - $ gtd batch due --has-due + $ mello batch due --has-due Bringing It all Together ^^^^^^^^^^^^^^^^^^^^^^^^ -What if you don't know what kind of action you want to take on a card before you invoke ``gtd``? Well, we provide a nice menu for you to work on each card in turn. The menu is kinda REPL-like so if you're a terminal power user (truly, why would you use this tool unless you're already a terminal power-user) it'll feel familiar. The menu is built using ``python-prompt-toolkit`` so it has nice tab-completion on every command available within it. You can type ``help`` at any time to view all the commands available within the REPL. +What if you don't know what kind of action you want to take on a card before you invoke ``mello``? Well, we provide a nice menu for you to work on each card in turn. The menu is kinda REPL-like so if you're a terminal power user (truly, why would you use this tool unless you're already a terminal power-user) it'll feel familiar. The menu is built using ``python-prompt-toolkit`` so it has nice tab-completion on every command available within it. You can type ``help`` at any time to view all the commands available within the REPL. Seeing is believing, so until I record a terminal session of me using it I'd highly encourage you to play around with this menu. It does some detection on the title of your card and will prompt you to move links out into attachments if appropriate. If the card doesn't have any tags yet, it'll prompt you to add some. :: # Work through cards in the "Inbox" list one at a time - $ gtd review -l Inbox + $ mello review -l Inbox # Review only cards from the "Today" list that have due dates - $ gtd review -l Today --has-due + $ mello review -l Today --has-due Setup @@ -129,16 +139,16 @@ Setup :: - $ pip install gtd.py - $ gtd onboard + $ pip install mello.py + $ mello onboard The ``onboard`` command will assist you through the process of getting a Trello API key for use with this program and putting it in the correct file. This will happen automatically if you run a command that requires authentication without having your API keys set. -If you'd like to enable automatic bash completion for gtd.py, add the following line to your ~/.bashrc: +If you'd like to enable automatic bash completion for mello.py, add the following line to your ~/.bashrc: :: - eval "$(_GTD_COMPLETE=source gtd)" + eval "$(_GTD_COMPLETE=source mello)" This relies on ``click``'s internal bash completion engine, so it does not work on other shells like ``sh``, ``csh``, or ``zsh``. @@ -161,18 +171,18 @@ There are other optional settings you can define inside your yaml configuration board: "Name of the Trello board you want to work with (case sensitive)" color: True # Do you want to show ANSI colors in the terminal? - banner: True # Do you want to see the "gtd.py" banner on each program run? + banner: True # Do you want to see the "mello.py" banner on each program run? All of these can be overridden on the command-line with the ``-b``, ``--no-color``, and ``--no-banner`` flags. -This configuration file can be put in a variety of locations within your home folder. The ``onboard`` command will help you with platform detection, putting the configuration file where appropriate given your operating system. When running, ``gtd``` will check all possible locations out of this list: +This configuration file can be put in a variety of locations within your home folder. The ``onboard`` command will help you with platform detection, putting the configuration file where appropriate given your operating system. When running, ``mello``` will check all possible locations out of this list: -* ``~/.gtd.yaml`` -* ``~/.config/gtd/gtd.yaml`` -* ``~/Library/Application Support/gtd/gtd.yaml`` -* ``~/.local/etc/gtd.yaml`` -* ``~/.local/etc/gtd/gtd.yaml`` +* ``~/.mello.yaml`` +* ``~/.config/mello/mello.yaml`` +* ``~/Library/Application Support/mello/mello.yaml`` +* ``~/.local/etc/mello.yaml`` +* ``~/.local/etc/mello/mello.yaml`` Notes ------ diff --git a/gtd.py b/mello.py similarity index 100% rename from gtd.py rename to mello.py diff --git a/setup.py b/setup.py index d5730c3..44aabf1 100644 --- a/setup.py +++ b/setup.py @@ -17,10 +17,10 @@ version=__version__, install_requires=reqs, packages=['todo'], - py_modules=['gtd'], + py_modules=['mello'], entry_points={ 'console_scripts': [ - 'mello = gtd:main' + 'mello = mello:main' ] }, python_requires='>=3', diff --git a/todo/__init__.py b/todo/__init__.py index e107160..97770eb 100644 --- a/todo/__init__.py +++ b/todo/__init__.py @@ -1,3 +1,3 @@ '''gtd.py''' -__version__ = '0.7.13' +__version__ = '0.7.16' __author__ = 'whitef0x0'