diff --git a/README.md b/README.md index e4a6cea..60f30b8 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,12 @@ Run `poetry run worblehat --help` for more info - [X] Ability to create and update bookcases - [X] Ability to create and update bookcase shelfs - [X] Ability to create and update bookcase items -- [X] Ability to borrow an item +- [X] Ability to borrow and deliver items +- [ ] Ability to borrow and deliver multiple items at a time - [X] Ability to enter the queue for borrowing an item - [ ] Ability to extend a borrowing, only if no one is behind you in the queue -- [ ] Ability to borrow multiple items at a time -- [ ] Ability to deliver multiple items at a time - [ ] Ability to list borrowed items which are overdue -- [ ] Ability to search for items +- [~] Ability to search for items - [ ] Ability to print PVV-specific labels for items missing a label, or which for any other reason needs a custom one - [ ] Ascii art of monkey with fingers in eyes diff --git a/worblehat/cli/main.py b/worblehat/cli/main.py index c82eb99..7d107d4 100644 --- a/worblehat/cli/main.py +++ b/worblehat/cli/main.py @@ -18,6 +18,7 @@ from .subclis import ( AdvancedOptionsCli, BookcaseItemCli, select_bookcase_shelf, + SearchCli, ) # TODO: Category seems to have been forgotten. Maybe relevant interactivity should be added? @@ -161,7 +162,13 @@ class WorblehatCli(NumberedCmd): def do_search(self, _: str): - print('TODO: implement search') + search_cli = SearchCli(self.sql_session) + search_cli.cmdloop() + if search_cli.result is not None: + BookcaseItemCli( + sql_session = self.sql_session, + bookcase_item = search_cli.result, + ).cmdloop() def do_advanced(self, _: str): diff --git a/worblehat/cli/prompt_utils.py b/worblehat/cli/prompt_utils.py index ca719dc..d2fac67 100644 --- a/worblehat/cli/prompt_utils.py +++ b/worblehat/cli/prompt_utils.py @@ -129,16 +129,16 @@ class NumberedCmd(Cmd): prompt_header: str | None = None + funcs: dict[int, dict[str, str | Callable[[Any, str], bool | None]]] def __init__(self): super().__init__() - @classmethod - def _generate_usage_list(cls) -> str: + def _generate_usage_list(self) -> str: result = '' - for i, func in cls.funcs.items(): + for i, func in self.funcs.items(): if i == 0: i = '*' result += f'{i}) {func["doc"]}\n' @@ -185,4 +185,27 @@ class NumberedCmd(Cmd): else: result += f'[{self.lastcmd}]> ' - return result \ No newline at end of file + return result + + +class NumberedItemSelector(NumberedCmd): + def __init__( + self, + items: list[Any], + stringify: Callable[[Any], str] = lambda x: str(x), + ): + super().__init__() + self.items = items + self.stringify = stringify + self.funcs = { + i: { + 'f': self._select_item, + 'doc': self.stringify(item), + } + for i, item in enumerate(items, start=1) + } + + + def _select_item(self, *a): + self.result = self.items[int(self.lastcmd)-1] + return True \ No newline at end of file diff --git a/worblehat/cli/subclis/__init__.py b/worblehat/cli/subclis/__init__.py index 2bef689..68eef45 100644 --- a/worblehat/cli/subclis/__init__.py +++ b/worblehat/cli/subclis/__init__.py @@ -1,3 +1,4 @@ from .advanced_options import AdvancedOptionsCli from .bookcase_item import BookcaseItemCli -from .bookcase_shelf_selector import select_bookcase_shelf \ No newline at end of file +from .bookcase_shelf_selector import select_bookcase_shelf +from .search import SearchCli \ No newline at end of file diff --git a/worblehat/cli/subclis/search.py b/worblehat/cli/subclis/search.py new file mode 100644 index 0000000..aff7499 --- /dev/null +++ b/worblehat/cli/subclis/search.py @@ -0,0 +1,145 @@ +from sqlalchemy import select +from sqlalchemy.orm import Session + + +from worblehat.cli.prompt_utils import ( + NumberedCmd, + NumberedItemSelector, +) +from worblehat.models import Author, BookcaseItem + + +class SearchCli(NumberedCmd): + def __init__(self, sql_session: Session): + super().__init__() + self.sql_session = sql_session + + + def do_search_all(self, _: str): + print('TODO: Implement search all') + + + def do_search_title(self, _: str): + while (input_text := input('Enter title: ')) == '': + pass + + items = self.sql_session.scalars( + select(BookcaseItem) + .where(BookcaseItem.name.ilike(f'%{input_text}%')), + ).all() + + if len(items) == 0: + print('No items found.') + return + + selector = NumberedItemSelector( + items = items, + stringify = lambda item: f"{item.name} ({item.isbn})", + ) + selector.cmdloop() + if selector.result is not None: + self.result = selector.result + return True + + + def do_search_author(self, _: str): + while (input_text := input('Enter author name: ')) == '': + pass + + author = self.sql_session.scalars( + select(Author) + .where(Author.name.ilike(f'%{input_text}%')), + ).all() + + if len(author) == 0: + print('No authors found.') + return + elif len(author) == 1: + selected_author = author[0] + print('Found author:') + print(f" {selected_author.name} ({sum(item.amount for item in selected_author.books)} items)") + else: + selector = NumberedItemSelector( + items = author, + stringify = lambda author: f"{author.name} ({sum(item.amount for item in author.books)} items)", + ) + selector.cmdloop() + if selector.result is None: + return + selected_author = selector.result + + selector = NumberedItemSelector( + items = selected_author.books, + stringify = lambda item: f"{item.name} ({item.isbn})", + ) + selector.cmdloop() + if selector.result is not None: + self.result = selector.result + return True + + + def do_search_owner(self, _: str): + while (input_text := input('Enter username: ')) == '': + pass + + users = self.sql_session.scalars( + select(BookcaseItem.owner) + .where(BookcaseItem.owner.ilike(f'%{input_text}%')) + .distinct(), + ).all() + + if len(users) == 0: + print('No users found.') + return + elif len(users) == 1: + selected_user = users[0] + print('Found user:') + print(f" {selected_user}") + else: + selector = NumberedItemSelector(items = users) + selector.cmdloop() + if selector.result is None: + return + selected_user = selector.result + + items = self.sql_session.scalars( + select(BookcaseItem) + .where(BookcaseItem.owner == selected_user), + ).all() + + selector = NumberedItemSelector( + items = items, + stringify = lambda item: f"{item.name} ({item.isbn})", + ) + selector.cmdloop() + if selector.result is not None: + self.result = selector.result + return True + + + def do_done(self, _: str): + return True + + + funcs = { + 1: { + 'f': do_search_all, + 'doc': 'Search everything', + }, + 2: { + 'f': do_search_title, + 'doc': 'Search by title', + }, + 3: { + 'f': do_search_author, + 'doc': 'Search by author', + }, + 4: { + 'f': do_search_owner, + 'doc': 'Search by owner', + }, + 9: { + 'f': do_done, + 'doc': 'Done', + }, + } \ No newline at end of file