Exemple #1
0
def delete_cards(config, force, noninteractive, tags, no_tags, match, listname, attachments, has_due):
    '''Delete a set of cards specified
    '''
    _, board = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    method = 'delete' if force else 'archive'
    if noninteractive:
        if force:
            [c.delete() for c in cards]
        else:
            [c.set_closed(True) for c in cards]
    else:
        for card in cards:
            display.show_card(card)
            if prompt_for_confirmation('Delete this card?'):
                if force:
                    card.delete()
                else:
                    card.set_closed(True)
                click.secho('Card {}d!'.format(method), fg='red')
Exemple #2
0
def review(config, tags, no_tags, match, listname, attachments, has_due,
           by_due):
    '''show a smart, command-line based menu for each card selected.
    This menu will prompt you to add tags to untagged cards, to attach the title
    of cards which have a link in the title, and gives you all the other functionality combined.
    '''
    connection, board = BoardTool.start(config)
    if by_due:
        cards = BoardTool.filter_cards(board, has_due_date=True)
        cards = sorted(cards, key=lambda c: c.due)
    else:
        cards = BoardTool.filter_cards(
            board,
            tags=tags,
            no_tags=no_tags,
            title_regex=match,
            list_regex=listname,
            has_attachments=attachments,
            has_due_date=has_due,
        )
    list_lookup = BoardTool.list_lookup(board)
    label_lookup = BoardTool.label_lookup(board)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        CardTool.smart_menu(card,
                            display.show_card,
                            list_lookup,
                            label_lookup,
                            color=config.color)
    click.echo('All done, have a great day!')
Exemple #3
0
def grep(config, pattern, insensitive, count, regexp):
    '''egrep through titles of cards on this board. This command attemps to replicate a couple of grep flags
    faithfully, so if you're a power-user of grep this command will feel familiar.
    '''
    if not (pattern or regexp):
        click.secho('No pattern provided to grep: use either the argument or -e', fg='red')
        raise GTDException(1)
    # Merge together the different regex arguments
    final_pattern = '|'.join(regexp) if regexp else ''
    if pattern and final_pattern:
        final_pattern = final_pattern + '|' + pattern
    elif pattern:
        final_pattern = pattern
    flags = re.I if insensitive else 0
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        title_regex=final_pattern,
        regex_flags=flags
    )
    if count:
        print(sum(1 for _ in cards))
        raise GTDException(0)
    display = Display(config.color)
    if config.banner:
        display.banner()
    display.show_cards(cards)
Exemple #4
0
def show_tags(config, json):
    '''Display all tags on this board'''
    _, board = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    tag_names = [t.name for t in board.get_labels()]
    display.show_raw(tag_names, use_json=json)
Exemple #5
0
def show_lists(config, json, show_all):
    '''Display all lists on this board'''
    _, board = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    list_filter = 'all' if show_all else 'open'
    list_names = [l.name for l in board.get_lists(list_filter)]
    display.show_raw(list_names, use_json=json)
Exemple #6
0
 def __init__(self, config: Configuration):
     self.config = config
     self.connection = TrelloConnection(config)
     self.display = Display(config, self.connection)
     self.board = self.connection.main_board()
     # 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))
Exemple #7
0
def batch_attach(config):
    '''Extract HTTP links from card titles'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board, title_regex=VALID_URL_REGEX)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Attach title?', True):
            CardTool.title_to_link(card)
Exemple #8
0
def add_card(config, title, message, edit, listname):
    '''Add a new card. If no title is provided, $EDITOR will be opened so you can write one.'''
    connection, board = BoardTool.start(config)
    if listname is None:
        inbox = BoardTool.get_inbox_list(connection, config)
    else:
        pattern = re.compile(listname, flags=re.I)
        target_lists = filter(lambda x: pattern.search(x.name),
                              board.get_lists('open'))
        try:
            inbox = next(target_lists)
        except StopIteration:
            click.secho('No list names matched by {}'.format(listname),
                        fg='red')
            raise GTDException(1)
    if not title:
        title = click.edit(require_save=True, text='<Title here>')
        if title is None:  # No changes were made in $EDITOR
            click.secho('No title entered for the new card!', fg='red')
            raise GTDException(1)
        else:
            title = title.strip()
    returned = inbox.add_card(name=title, desc=message)
    if edit:
        display = Display(config.color)
        list_lookup = BoardTool.list_lookup(board)
        label_lookup = BoardTool.label_lookup(board)
        CardTool.smart_menu(returned,
                            display.show_card,
                            list_lookup,
                            label_lookup,
                            color=config.color)
    else:
        click.secho('Successfully added card {0}!'.format(returned),
                    fg='green')
Exemple #9
0
def validate_fields(command_context, param, value):
    valid = Display.valid_fields()
    possible = value.split(',') if value else []
    for field in possible:
        if field not in valid:
            raise click.BadParameter(
                f'Field {field} is not a valid field! Use {",".join(valid)}')
    return possible
Exemple #10
0
def batch_tag(config, tags, no_tags, match, listname, attachments, has_due):
    '''Change tags on each card selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        CardTool.add_labels(card)
Exemple #11
0
def validate_fields(ctx, param, value):
    valid = Display.build_fields().keys()
    possible = value.split(',') if value else []
    for field in possible:
        if field not in valid:
            raise click.BadParameter(
                'Field {} is not a valid field! Use {}'.format(
                    field, ','.join(valid)))
    return possible
Exemple #12
0
def batch_move(config, tags, no_tags, match, listname, attachments, has_due):
    '''Change the list of each card selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Want to move this one?', True):
            CardTool.move_to_list(card)
Exemple #13
0
def batch_due(config, tags, no_tags, match, listname, attachments, has_due):
    '''Set due date for all cards selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Set due date?'):
            CardTool.set_due_date(card)
Exemple #14
0
def show_cards(config, json, tsv, tags, no_tags, match, listname, attachments, has_due):
    '''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)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    display.show_cards(cards, use_json=json, tsv=tsv)
Exemple #15
0
def show_soon(config, json, tsv):
    _, board = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    cards = BoardTool.filter_cards(board, has_due_date=True)
    display.show_cards(cards, use_json=json, tsv=tsv, sort='due')
Exemple #16
0
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, current_user = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    cards = BoardTool.filter_cards(
        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,
        assigned=assigned,
        completed=completed,
        include_closed=include_closed
    )
    display.show_cards(cards, use_json=json, tsv=tsv)
Exemple #17
0
def grep(config, pattern, insensitive, count, regexp, by, fields, json):
    '''egrep through titles of cards on this board. This command attemps to replicate a couple of grep flags
    faithfully, so if you're a power-user of grep this command will feel familiar.
    One deviation from grep is the --json flag, which outputs all matching cards in full JSON format.
    '''
    if not (pattern or regexp):
        click.secho(
            'No pattern provided to grep: use either the argument or -e',
            fg='red')
        raise GTDException(1)
    # Merge together the different regex arguments
    final_pattern = '|'.join(regexp) if regexp else ''
    if pattern and final_pattern:
        final_pattern = final_pattern + '|' + pattern
    elif pattern:
        final_pattern = pattern
    flags = re.I if insensitive else 0
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board,
                                   title_regex=final_pattern,
                                   regex_flags=flags)
    if count:
        print(sum(1 for _ in cards))
        return
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    display.show_cards(cards, use_json=json, sort=by, table_fields=fields)
Exemple #18
0
def delete_cards(config, force, noninteractive, tags, no_tags, match, listname, attachments, has_due):
    '''Delete a set of cards specified
    '''
    _, board, _ = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    method = 'delete' if force else 'archive'
    if noninteractive:
        if force:
            [c.delete() for c in cards]
        else:
            [c.set_closed(True) for c in cards]
    else:
        for card in cards:
            display.show_card(card)
            if prompt_for_confirmation('Delete this card?'):
                if force:
                    card.delete()
                else:
                    card.set_closed(True)
                click.secho('Card {}d!'.format(method), fg='red')
Exemple #19
0
def grep(config, pattern, insensitive, count, regexp):
    '''egrep through titles of cards on this board. This command attemps to replicate a couple of grep flags
    faithfully, so if you're a power-user of grep this command will feel familiar.
    '''
    if not (pattern or regexp):
        click.secho('No pattern provided to grep: use either the argument or -e', fg='red')
        raise GTDException(1)
    # Merge together the different regex arguments
    final_pattern = '|'.join(regexp) if regexp else ''
    if pattern and final_pattern:
        final_pattern = final_pattern + '|' + pattern
    elif pattern:
        final_pattern = pattern
    flags = re.I if insensitive else 0
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        title_regex=final_pattern,
        regex_flags=flags
    )
    if count:
        print(sum(1 for _ in cards))
        raise GTDException(0)
    display = Display(config.color)
    if config.banner:
        display.banner()
    display.show_cards(cards)
Exemple #20
0
def show_unresponded(config, json):
    '''Display all unresponded comments for current account'''
    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)
Exemple #21
0
def show_tags(config, json):
    '''Display all tags on this board'''
    _, board, _ = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    tag_names = [t.name for t in board.get_labels()]
    display.show_raw(tag_names, use_json=json)
Exemple #22
0
def review(config, tags, no_tags, match, listname, attachments, has_due):
    '''show a smart, command-line based menu for each card selected.
    This menu will prompt you to add tags to untagged cards, to attach the title
    of cards which have a link in the title, and gives you all the other functionality combined.
    '''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(
        board,
        tags=tags,
        no_tags=no_tags,
        title_regex=match,
        list_regex=listname,
        has_attachments=attachments,
        has_due_date=has_due
    )
    list_lookup = BoardTool.list_lookup(board)
    label_lookup = BoardTool.label_lookup(board)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        CardTool.smart_menu(card, display.show_card, list_lookup, label_lookup, Colors.yellow)
    click.echo('All done, have a great day!')
Exemple #23
0
def show_lists(config, json, show_all):
    '''Display all lists on this board'''
    _, board, _ = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    list_filter = 'all' if show_all else 'open'
    list_names = [l.name for l in board.get_lists(list_filter)]
    display.show_raw(list_names, use_json=json)
Exemple #24
0
def batch_attach(config):
    '''Extract HTTP links from card titles'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board, title_regex=VALID_URL_REGEX)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Attach title?', True):
            CardTool.title_to_link(card)
Exemple #25
0
def add_card(config, title, message, edit):
    '''Add a new card. If no title is provided, $EDITOR will be opened so you can write one.'''
    connection, board = BoardTool.start(config)
    inbox = BoardTool.get_inbox_list(connection, config)
    if not title:
        title = click.edit(require_save=True, text='<Title here>')
        if title is None:  # No changes were made in $EDITOR
            click.secho('No title entered for the new card!', fg='red')
            raise GTDException(1)
        else:
            title = title.strip()
    returned = inbox.add_card(name=title, desc=message)
    if edit:
        display = Display(config.color)
        list_lookup = BoardTool.list_lookup(board)
        label_lookup = BoardTool.label_lookup(board)
        CardTool.smart_menu(returned, display.show_card, list_lookup, label_lookup, Colors.yellow)
    else:
        click.secho('Successfully added card {0}!'.format(returned), fg='green')
Exemple #26
0
def batch_tag(config, tags, no_tags, match, listname, attachments, has_due):
    '''Change tags on each card selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board,
                                   tags=tags,
                                   no_tags=no_tags,
                                   title_regex=match,
                                   list_regex=listname,
                                   has_attachments=attachments,
                                   has_due_date=has_due)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        CardTool.add_labels(card)
Exemple #27
0
def batch_due(config, tags, no_tags, match, listname, attachments, has_due):
    '''Set due date for all cards selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board,
                                   tags=tags,
                                   no_tags=no_tags,
                                   title_regex=match,
                                   list_regex=listname,
                                   has_attachments=attachments,
                                   has_due_date=has_due)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Set due date?'):
            CardTool.set_due_date(card)
Exemple #28
0
def batch_move(config, tags, no_tags, match, listname, attachments, has_due):
    '''Change the list of each card selected'''
    connection, board = BoardTool.start(config)
    cards = BoardTool.filter_cards(board,
                                   tags=tags,
                                   no_tags=no_tags,
                                   title_regex=match,
                                   list_regex=listname,
                                   has_attachments=attachments,
                                   has_due_date=has_due)
    display = Display(config.color)
    if config.banner:
        display.banner()
    for card in cards:
        display.show_card(card)
        if prompt_for_confirmation('Want to move this one?', True):
            CardTool.move_to_list(card)
Exemple #29
0
def show_boards(config, json, tsv, by, show_all):
    '''Show all boards your account can access'''
    connection, board = BoardTool.start(config)
    display = Display(config.color)
    if config.banner and not json:
        display.banner()
    if show_all:
        boards = connection.trello.fetch_json('/members/me/boards/?filter=all')
    else:
        boards = connection.boards
    if json:
        display.show_raw(boards, use_json=json)
        return
    # Set up a table to hold our boards
    board_columns = ['name', 'activity', 'members', 'permission', 'url']
    if by not in board_columns:
        click.secho('Field {} is not a valid field: {}'.format(
            by, ','.join(board_columns)),
                    fg='red')
        raise GTDException(1)
    table = prettytable.PrettyTable()
    table.field_names = board_columns
    table.align = 'l'
    if tsv:
        table.set_style(prettytable.PLAIN_COLUMNS)
    else:
        table.hrules = prettytable.FRAME
    for b in boards:
        table.add_row([
            b['name'],
            b['dateLastActivity'] or '',
            len(b['memberships']),
            b['prefs']['permissionLevel'],
            b['shortUrl'],
        ])
    try:
        table[0]
    except IndexError:
        click.secho('You have no boards!', fg='red')
    print(table.get_string(sortby=by))
Exemple #30
0
class CLIContext:
    '''CLIContext is a container for the commonly used objects in each gtd command.
    It is passed around as an argument injected by click after the cli() function runs.
    Any reference to "ctx" as a parameter in this file is an instance of CLIContext.

    config: todo.configuration.Configuration
    connection: todo.connection.TrelloConnection
    display: todo.display.Display
    '''
    def __init__(self, config: Configuration):
        self.config = config
        self.connection = TrelloConnection(config)
        self.display = Display(config, self.connection)
        self.board = self.connection.main_board()
        # 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))

    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'
                )

    @return_on_eof
    def move_between_boards(self, card: Card) -> None:
        boards_by_name = self.connection.boards_by_name()
        board_name = prompt('gtd.py > move > board name? ',
                            completer=FuzzyWordCompleter(
                                boards_by_name.keys()))
        board_id = boards_by_name[board_name]['id']
        lists_json = self.connection.trello.fetch_json(
            f'/boards/{board_id}/lists?cards=none&filter=open&fields=name')
        name_to_listid = {l['name']: l['id'] for l in lists_json}
        list_name = prompt(
            f'gtd.py > move > {board_name} > list name? ',
            completer=FuzzyWordCompleter(name_to_listid.keys()),
        )
        card.change_board(board_id, list_id=name_to_listid[list_name])
        click.secho(f'Changed list to {list_name} on {board_name}', fg='green')
Exemple #31
0
def validate_sort(ctx, param, value):
    if value and value not in Display.build_fields().keys():
        raise click.BadParameter(
            'Sort parameter {} is not a valid field!'.format(value))
    return value
Exemple #32
0
def validate_sort(command_context, param, value):
    if value and value not in Display.valid_fields():
        raise click.BadParameter(
            f'Sort parameter {value} is not a valid field!')
    return value