def manipulate_attachments(card): '''Give the user a CRUD interface for attachments on this card''' print('Enter a URL, "delete", "open", "print", or Enter to exit') user_input = 'Nothing really' attachment_completer = WordCompleter( ['delete', 'print', 'open', 'http://', 'https://'], ignore_case=True) while user_input != '': user_input = prompt('attach > ', completer=attachment_completer).strip() if re.search(VALID_URL_REGEX, user_input): # attach this link card.attach(url=user_input) print('Attached {0}'.format(user_input)) elif user_input in ['delete', 'open']: attachment_opts = {a.name: a for a in card.get_attachments()} if not attachment_opts: print('No attachments') continue dest = single_select(attachment_opts.keys()) if dest is not None: target = attachment_opts[dest] if user_input == 'delete': card.remove_attachment(target.id) elif user_input == 'open': with DevNullRedirect(): webbrowser.open(target.url) elif user_input == 'print': existing_attachments = card.get_attachments() if existing_attachments: print('Attachments:') for a in existing_attachments: print(' ' + a.name)
def manipulate_attachments(self): '''Give the user a CRUD interface for attachments on this card''' print('Enter a URL, "delete", "open", or "print". Ctrl+D to exit') attachment_completer = WordCompleter(['delete', 'print', 'open', 'http://', 'https://'], ignore_case=True) while True: user_input = prompt('gtd.py > attach > ', completer=attachment_completer).strip() if re.search(VALID_URL_REGEX, user_input): # attach this link self.attach_url(user_input) print(f'Attached {user_input}') elif user_input in ['delete', 'open']: attachment_opts = {a['name']: a for a in self.fetch_attachments()} if not attachment_opts: print('This card is free of attachments') continue dest = single_select(attachment_opts.keys()) if dest is not None: target = attachment_opts[dest] if user_input == 'delete': self.remove_attachment(target['id']) self.fetch_attachments(force=True) elif user_input == 'open': with DevNullRedirect(): webbrowser.open(target['url']) elif user_input == 'print': existing_attachments = self.fetch_attachments(force=True) if existing_attachments: print('Attachments:') for a in existing_attachments: print(' ' + a['name'])
def onboard(no_open, output_path=None): '''Obtain Trello API credentials and put them into your config file. This is invoked automatically the first time you attempt to do an operation which requires authentication. The configuration file is put in an appropriate place for your operating system. If you want to change it later, you can use `gtd config -e` to open it in $EDITOR. ''' output_file = output_path or Configuration.suggest_config_location() # Use platform detection user_api_key_url = 'https://trello.com/app-key' request_token_url = 'https://trello.com/1/OAuthGetRequestToken' authorize_url = 'https://trello.com/1/OAuthAuthorizeToken' access_token_url = 'https://trello.com/1/OAuthGetAccessToken' # First, open the URL that allows the user to get an auth token. Tell them to copy both into the program click.echo('Welcome to gtd.py! To get started, open the following URL in your web browser:') click.echo(' ' + user_api_key_url) click.echo('When you arrive at that page, log in and copy the "Key" displayed in a text box.') if not no_open: with DevNullRedirect(): webbrowser.open_new_tab(user_api_key_url) api_key = click.prompt('Please enter the value for "Key"', confirmation_prompt=True) click.echo('Now scroll to the bottom of the page and copy the "Secret" shown in a text box.') api_secret = click.prompt('Please enter the value for "Secret"', confirmation_prompt=True) # Then, work on getting OAuth credentials for the user so they are permanently authorized to use this program click.echo('We will now get the OAuth credentials necessary to use this program...') # The following code is cannibalized from trello.util.create_oauth_token from the py-trello project. # Rewriting because it does not support opening the auth URLs using webbrowser.open and since we're using # click, a lot of the input methods used in that script are simplistic compared to what's available to us. # Thank you to the original authors! '''Step 1: Get a request token. This is a temporary token that is used for having the user authorize an access token and to sign the request to obtain said access token.''' session = OAuth1Session(client_key=api_key, client_secret=api_secret) try: response = session.fetch_request_token(request_token_url) except TokenRequestDenied: click.secho('Invalid API key/secret provided: {0} / {1}'.format(api_key, api_secret), fg='red') sys.exit(1) resource_owner_key, resource_owner_secret = response.get('oauth_token'), response.get('oauth_token_secret') '''Step 2: Redirect to the provider. Since this is a CLI script we do not redirect. In a web application you would redirect the user to the URL below.''' user_confirmation_url = '{authorize_url}?oauth_token={oauth_token}&scope={scope}&expiration={expiration}&name={name}'.format( authorize_url=authorize_url, oauth_token=resource_owner_key, expiration='never', scope='read,write', name='gtd.py' ) click.echo('Visit the following URL in your web browser to authorize gtd.py to access your account:') click.echo(' ' + user_confirmation_url) if not no_open: with DevNullRedirect(): webbrowser.open_new_tab(user_confirmation_url) '''After the user has granted access to you, the consumer, the provider will redirect you to whatever URL you have told them to redirect to. You can usually define this in the oauth_callback argument as well.''' while not click.confirm('Have you authorized gtd.py?', default=False): pass oauth_verifier = click.prompt('What is the Verification code?').strip() '''Step 3: Once the consumer has redirected the user back to the oauth_callback URL you can request the access token the user has approved. You use the request token to sign this request. After this is done you throw away the request token and use the access token returned. You should store this access token somewhere safe, like a database, for future use.''' session = OAuth1Session(client_key=api_key, client_secret=api_secret, resource_owner_key=resource_owner_key, resource_owner_secret=resource_owner_secret, verifier=oauth_verifier) access_token = session.fetch_access_token(access_token_url) final_output_data = { 'oauth_token': access_token['oauth_token'], 'oauth_token_secret': access_token['oauth_token_secret'], 'api_key': api_key, 'api_secret': api_secret, 'color': True, 'banner': False } # Ensure we have a folder to put this in, if it's in a nested config location output_folder = os.path.dirname(output_file) if not os.path.isdir(output_folder): os.makedirs(output_folder) click.echo('Created folder {0} to hold your configuration'.format(output_folder)) # Try to be safe about not destroying existing credentials if os.path.exists(output_file): if click.confirm('{0} exists already, would you like to back it up?'.format(output_file), default=True): shutil.move(output_file, output_file + '.backup') if not click.confirm('Overwrite the existing file?', default=False): return with open(output_file, 'w') as f: f.write(yaml.safe_dump(final_output_data, default_flow_style=False)) click.echo('Credentials saved in "{0}"- you can now use gtd.py!'.format(output_file)) click.echo('Use the "config" command to view or edit your configuration file')
def smart_menu(card, f_display, list_choices, label_choices, color=None): '''make assumptions about what you want to do with a card and ask the user if they want to''' on = color if color else '' off = Colors.reset if color else '' card.fetch() f_display(card) if card.get_attachments(): if prompt_for_confirmation( '{0}Open attachments?{1}'.format(on, off), False): with DevNullRedirect(): for url in [ a.url for a in card.get_attachments() if a.url is not None ]: webbrowser.open(url) if re.search(VALID_URL_REGEX, card.name): if prompt_for_confirmation( '{0}Link in title detected, want to attach it & rename?{1}' .format(on, off), True): CardTool.title_to_link(card) if not card.list_labels: print('{0}No tags on this card yet, want to add some?{1}'.format( on, off)) CardTool.add_labels(card, label_choices) commands = { 'archive': 'mark this card as closed', 'attach': 'add, delete, or open attachments', 'comment': 'add a comment to this card', 'delete': 'permanently delete this card', 'duedate': 'add a due date or change the due date', 'description': 'change the description of this card (desc)', 'help': 'display this help output (h)', 'move': 'move to a different list (m)', 'next': 'move on to the next card (n)', 'open': 'open all links on this card (o)', 'print': 'display this card (p)', 'rename': 'change title of this card', 'tag': 'add or remove tags on this card (t)', 'quit': 'exit program' } command_completer = WordCompleter(commands.keys()) while True: user_input = prompt('gtd.py > ', completer=command_completer) if user_input in ['q', 'quit']: raise GTDException(0) elif user_input in ['n', 'next']: break elif user_input in ['p', 'print']: card.fetch() f_display(card) elif user_input in ['o', 'open']: with DevNullRedirect(): for url in [ a.url for a in card.get_attachments() if a.url is not None ]: webbrowser.open(url) elif user_input in ['desc', 'description']: if CardTool.change_description(card): print('Description changed!') elif user_input == 'delete': card.delete() print('Card deleted') break elif user_input == 'attach': CardTool.manipulate_attachments(card) elif user_input == 'archive': card.set_closed(True) print('Card archived') break elif user_input in ['t', 'tag']: CardTool.add_labels(card, label_choices) elif user_input == 'rename': CardTool.rename(card) elif user_input == 'duedate': CardTool.set_due_date(card) elif user_input in ['h', 'help']: for cname, cdesc in commands.items(): print('{0:<16}| {1}{2}{3}'.format(cname, on, cdesc, off)) elif user_input in ['m', 'move']: if CardTool.move_to_list(card, list_choices): break elif user_input == 'comment': new_comment = click.edit(text='<Comment here>', require_save=True) if new_comment: card.comment(new_comment) else: click.secho('Change the text & save to post the comment', fg='red') else: print( '{0}{1}{2} is not a command, type "{0}help{2}" to view available commands' .format(on, user_input, off))
def card_repl(self, card: dict) -> bool: '''card_repl displays a command-prompt based UI for modifying a card, with tab-completion and suggestions. It is the logic behind "gtd review" and the "-e" flag in "gtd add" It makes assumptions about what a user might want to do with a card: - Are there attachments? Maybe you want to open them. - Does there appear to be a URL in the title? You might want to attach it. - Are there no tags? Maybe you want to add some. Returns: boolean: move forwards or backwards in the deck of cards ''' on = Colors.yellow if self.config.color else '' off = Colors.reset if self.config.color else '' self.display.show_card(card) if self.config.prompt_for_open_attachments and card['badges'][ 'attachments']: if prompt_for_confirmation(f'{on}Open attachments?{off}', False): with DevNullRedirect(): for url in [ a['url'] for a in card.fetch_attachments() if a['url'] ]: webbrowser.open(url) if re.search(VALID_URL_REGEX, card['name']): if prompt_for_confirmation( f'{on}Link in title detected, want to attach it & rename?{off}', True): card.title_to_link() if self.config.prompt_for_untagged_cards and not card['labels']: print(f'{on}No tags on this card yet, want to add some?{off}') card.add_labels(self._label_choices) commands = { 'archive': 'mark this card as closed', 'attach': 'add, delete, or open attachments', 'change-list': 'move this to a different list on the same board', 'comment': 'add a comment to this card', 'delete': 'permanently delete this card', 'duedate': 'add a due date or change the due date', 'description': 'change the description of this card (desc)', 'help': 'display this help output (h)', 'move': 'move to a different board and list (m)', 'next': 'move to the next card (n)', 'open': 'open all links on this card (o)', 'prev': 'go back to the previous card (p)', 'print': 're-display this card', 'rename': 'change title of this card', 'tag': 'add or remove tags on this card (t)', 'unarchive': 'mark this card as open', 'quit': 'exit program', } command_completer = FuzzyWordCompleter(commands.keys()) while True: user_input = prompt('gtd.py > ', completer=command_completer) if user_input in ['q', 'quit']: raise GTDException(0) elif user_input in ['n', 'next']: return True elif user_input in ['p', 'prev']: return False elif user_input == 'print': card.fetch() self.display.show_card(card) elif user_input in ['o', 'open']: with DevNullRedirect(): for url in [ a['url'] for a in card.fetch_attachments() if a['url'] is not None ]: webbrowser.open(url) elif user_input in ['desc', 'description']: card.change_description() elif user_input == 'delete': card.delete() print('Card deleted') return True elif user_input == 'attach': card.manipulate_attachments() elif user_input == 'archive': card.set_closed(True) print('Card archived') return True elif user_input == 'unarchive': card.set_closed(False) print('Card returned to board') elif user_input in ['t', 'tag']: card.add_labels(self._label_choices) elif user_input == 'rename': # TODO optional form 'rename New name of card' card.rename() elif user_input == 'duedate': card.set_due_date() elif user_input in ['h', 'help']: for cname, cdesc in commands.items(): print('{0:<16}| {1}{2}{3}'.format(cname, on, cdesc, off)) elif user_input == 'change-list': if card.move_to_list(self._list_choices): return True elif user_input in ['m', 'move']: self.move_between_boards(card) elif user_input == 'comment': # TODO Optional form 'comment Contents of a comment' new_comment = click.edit(text='<Comment here>', require_save=True) if new_comment: card.comment(new_comment) else: click.secho('Change the text & save to post the comment', fg='red') else: print( f'{on}{user_input}{off} is not a command, type "{on}help{off}" to view available commands' )