diff --git a/worblehat/flaskapp.py b/worblehat/flaskapp.py index a1b10fc..5556d93 100644 --- a/worblehat/flaskapp.py +++ b/worblehat/flaskapp.py @@ -32,7 +32,7 @@ def configure_admin(app): admin.add_view(ModelView(Author, db.session)) admin.add_view(ModelView(Bookcase, db.session)) admin.add_view(ModelView(BookcaseItem, db.session)) - admin.add_view(ModelView(BookcaseLocation, db.session)) + admin.add_view(ModelView(BookcaseShelf, db.session)) admin.add_view(ModelView(Category, db.session)) admin.add_view(ModelView(Language, db.session)) admin.add_view(ModelView(MediaType, db.session)) \ No newline at end of file diff --git a/worblehat/models/Bookcase.py b/worblehat/models/Bookcase.py index c9c4865..6a3e854 100644 --- a/worblehat/models/Bookcase.py +++ b/worblehat/models/Bookcase.py @@ -14,12 +14,12 @@ from .mixins import ( UniqueNameMixin, ) if TYPE_CHECKING: - from .BookcaseLocation import BookcaseLocation + from .BookcaseShelf import BookcaseShelf class Bookcase(Base, UidMixin, UniqueNameMixin): description: Mapped[str | None] = mapped_column(Text) - locations: Mapped[list[BookcaseLocation]] = relationship(back_populates='bookcase') + shelfs: Mapped[list[BookcaseShelf]] = relationship(back_populates='bookcase') def __init__( self, diff --git a/worblehat/models/BookcaseItem.py b/worblehat/models/BookcaseItem.py index 0471c8b..0d36e5a 100644 --- a/worblehat/models/BookcaseItem.py +++ b/worblehat/models/BookcaseItem.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from sqlalchemy import ( Integer, + SmallInteger, String, ForeignKey, ) @@ -23,23 +24,22 @@ from .xref_tables import ( ) if TYPE_CHECKING: from .Author import Author - from .BookcaseLocation import BookcaseLocation + from .BookcaseShelf import BookcaseShelf from .Category import Category from .Language import Language from .MediaType import MediaType class BookcaseItem(Base, UidMixin, UniqueNameMixin): - # NOTE: This is kept non-unique in case we have - # multiple copies of the same book. - isbn: Mapped[int | None] = mapped_column(String, index=True) + isbn: Mapped[int] = mapped_column(String, unique=True, index=True) owner: Mapped[str] = mapped_column(String, default='PVV') + amount: Mapped[int] = mapped_column(SmallInteger, default=1) fk_media_type_uid: Mapped[int] = mapped_column(Integer, ForeignKey('MediaType.uid')) - fk_bookcase_location_uid: Mapped[int | None] = mapped_column(Integer, ForeignKey('BookcaseLocation.uid')) + fk_bookcase_shelf_uid: Mapped[int | None] = mapped_column(Integer, ForeignKey('BookcaseShelf.uid')) fk_language_uid: Mapped[int] = mapped_column(Integer, ForeignKey('Language.uid')) media_type: Mapped[MediaType] = relationship(back_populates='items') - location: Mapped[BookcaseLocation] = relationship(back_populates='items') + shelf: Mapped[BookcaseShelf] = relationship(back_populates='items') language: Mapped[Language] = relationship() categories: Mapped[set[Category]] = relationship( diff --git a/worblehat/models/BookcaseLocation.py b/worblehat/models/BookcaseShelf.py similarity index 52% rename from worblehat/models/BookcaseLocation.py rename to worblehat/models/BookcaseShelf.py index 843f350..69bea55 100644 --- a/worblehat/models/BookcaseLocation.py +++ b/worblehat/models/BookcaseShelf.py @@ -4,7 +4,9 @@ from typing import TYPE_CHECKING from sqlalchemy import ( Integer, ForeignKey, + SmallInteger, Text, + UniqueConstraint, ) from sqlalchemy.orm import ( Mapped, @@ -13,28 +15,39 @@ from sqlalchemy.orm import ( ) from .Base import Base -from .mixins import ( - UidMixin, - UniqueNameMixin, -) +from .mixins import UidMixin if TYPE_CHECKING: from .Bookcase import Bookcase from .BookcaseItem import BookcaseItem -class BookcaseLocation(Base, UidMixin, UniqueNameMixin): +# NOTE: Booshelfs are 0 indexed for both rows and columns, +# where cell 0-0 is placed in the lower right corner. + +class BookcaseShelf(Base, UidMixin): + __table_args__ = ( + UniqueConstraint( + 'column', + 'fk_bookcase_uid', + 'row', + ), + ) description: Mapped[str | None] = mapped_column(Text) + row: Mapped[int] = mapped_column(SmallInteger) + column: Mapped[int] = mapped_column(SmallInteger) fk_bookcase_uid: Mapped[int] = mapped_column(Integer, ForeignKey('Bookcase.uid')) - bookcase: Mapped[Bookcase] = relationship(back_populates='locations') - items: Mapped[set[BookcaseItem]] = relationship(back_populates='location') + bookcase: Mapped[Bookcase] = relationship(back_populates='shelfs') + items: Mapped[set[BookcaseItem]] = relationship(back_populates='shelf') def __init__( self, - name: str, + row: int, + column: int, bookcase: Bookcase, description: str | None = None, ): - self.name = name + self.row = row + self.column = column self.bookcase = bookcase self.description = description \ No newline at end of file diff --git a/worblehat/models/Language.py b/worblehat/models/Language.py index 0da2e96..c470bc2 100644 --- a/worblehat/models/Language.py +++ b/worblehat/models/Language.py @@ -12,7 +12,7 @@ from .Base import Base from .mixins import UidMixin, UniqueNameMixin class Language(Base, UidMixin, UniqueNameMixin): - iso639_1_code: Mapped[str] = mapped_column(String(2)) + iso639_1_code: Mapped[str] = mapped_column(String(2), unique=True, index=True) def __init__( self, diff --git a/worblehat/models/__init__.py b/worblehat/models/__init__.py index 085c4b7..f3f74f8 100644 --- a/worblehat/models/__init__.py +++ b/worblehat/models/__init__.py @@ -2,7 +2,7 @@ from .Author import Author from .Base import Base from .Bookcase import Bookcase from .BookcaseItem import BookcaseItem -from .BookcaseLocation import BookcaseLocation +from .BookcaseShelf import BookcaseShelf from .Category import Category from .Language import Language from .MediaType import MediaType \ No newline at end of file diff --git a/worblehat/seed_test_data.py b/worblehat/seed_test_data.py index 57b4f29..aeccbb0 100644 --- a/worblehat/seed_test_data.py +++ b/worblehat/seed_test_data.py @@ -5,7 +5,7 @@ from flask_sqlalchemy import SQLAlchemy from .models import ( Bookcase, - BookcaseLocation, + BookcaseShelf, Language, MediaType, ) @@ -19,15 +19,100 @@ def seed_data(db: SQLAlchemy): ] bookcases = [ - Bookcase(name='A', description='The first bookcase'), - Bookcase(name='B', description='The second bookcase'), + Bookcase(name='Unnamed A', description='White case across dibbler'), + Bookcase(name='Unnamed B', description='Math case in the working room'), + Bookcase(name='Unnamed C', description='Large case in the working room'), + Bookcase(name='Unnamed D', description='White comics case in the hallway'), + Bookcase(name='Unnamed E', description='Wooden comics case in the hallway'), ] - locations = [ - BookcaseLocation(name='1-1', description='The first location', bookcase=bookcases[0]), - BookcaseLocation(name='1-2', description='The second location', bookcase=bookcases[0]), - BookcaseLocation(name='2-1', description='The third location', bookcase=bookcases[1]), - BookcaseLocation(name='2-2', description='The fourth location', bookcase=bookcases[1]), + shelfs = [ + BookcaseShelf(row=0, column=0, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=0, bookcase=bookcases[0]), + BookcaseShelf(row=2, column=0, bookcase=bookcases[0]), + BookcaseShelf(row=3, column=0, bookcase=bookcases[0], description="Hacking"), + BookcaseShelf(row=4, column=0, bookcase=bookcases[0], description="Hacking"), + + BookcaseShelf(row=0, column=1, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=1, bookcase=bookcases[0]), + BookcaseShelf(row=2, column=1, bookcase=bookcases[0], description="DOS"), + BookcaseShelf(row=3, column=1, bookcase=bookcases[0], description="Food for thought"), + BookcaseShelf(row=4, column=1, bookcase=bookcases[0], description="CPP"), + + BookcaseShelf(row=0, column=2, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=2, bookcase=bookcases[0]), + BookcaseShelf(row=2, column=2, bookcase=bookcases[0], description="E = mc2"), + BookcaseShelf(row=3, column=2, bookcase=bookcases[0], description="OBJECTION!"), + BookcaseShelf(row=4, column=2, bookcase=bookcases[0], description="/home"), + + BookcaseShelf(row=0, column=3, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=3, bookcase=bookcases[0], description="Big indonisian island"), + BookcaseShelf(row=2, column=3, bookcase=bookcases[0]), + BookcaseShelf(row=3, column=3, bookcase=bookcases[0], description="Div science"), + BookcaseShelf(row=4, column=3, bookcase=bookcases[0], description="/home"), + + BookcaseShelf(row=0, column=4, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=4, bookcase=bookcases[0]), + BookcaseShelf(row=2, column=4, bookcase=bookcases[0], description="(not) computer vision"), + BookcaseShelf(row=3, column=4, bookcase=bookcases[0], description="Low voltage"), + BookcaseShelf(row=4, column=4, bookcase=bookcases[0], description="/home"), + + BookcaseShelf(row=0, column=5, bookcase=bookcases[0]), + BookcaseShelf(row=1, column=5, bookcase=bookcases[0]), + BookcaseShelf(row=2, column=5, bookcase=bookcases[0], description="/home"), + BookcaseShelf(row=3, column=5, bookcase=bookcases[0], description="/home"), + + BookcaseShelf(row=0, column=0, bookcase=bookcases[1]), + BookcaseShelf(row=1, column=0, bookcase=bookcases[1], description="Kjellerarealer og komodovaraner"), + BookcaseShelf(row=2, column=0, bookcase=bookcases[1]), + BookcaseShelf(row=3, column=0, bookcase=bookcases[1], description="Quick mafs"), + BookcaseShelf(row=4, column=0, bookcase=bookcases[1]), + + BookcaseShelf(row=0, column=0, bookcase=bookcases[2]), + BookcaseShelf(row=1, column=0, bookcase=bookcases[2]), + BookcaseShelf(row=2, column=0, bookcase=bookcases[2], description="AI"), + BookcaseShelf(row=3, column=0, bookcase=bookcases[2], description="X86"), + BookcaseShelf(row=4, column=0, bookcase=bookcases[2], description="Humanoira"), + BookcaseShelf(row=5, column=0, bookcase=bookcases[2], description="Hvem monterte rørforsterker?"), + + BookcaseShelf(row=0, column=1, bookcase=bookcases[2]), + BookcaseShelf(row=1, column=1, bookcase=bookcases[2], description="Div data"), + BookcaseShelf(row=2, column=1, bookcase=bookcases[2], description="Chemistry"), + BookcaseShelf(row=3, column=1, bookcase=bookcases[2], description="Soviet Phys. Techn. Phys"), + BookcaseShelf(row=4, column=1, bookcase=bookcases[2], description="Digitalteknikk"), + BookcaseShelf(row=5, column=1, bookcase=bookcases[2], description="Material"), + + BookcaseShelf(row=0, column=2, bookcase=bookcases[2]), + BookcaseShelf(row=1, column=2, bookcase=bookcases[2], description="Assembler / APL"), + BookcaseShelf(row=2, column=2, bookcase=bookcases[2], description="Internet"), + BookcaseShelf(row=3, column=2, bookcase=bookcases[2], description="Algorithms"), + BookcaseShelf(row=4, column=2, bookcase=bookcases[2], description="Soviet Physics Jetp"), + BookcaseShelf(row=5, column=2, bookcase=bookcases[2], description="Død og pine"), + + BookcaseShelf(row=0, column=3, bookcase=bookcases[2]), + BookcaseShelf(row=1, column=3, bookcase=bookcases[2], description="Web"), + BookcaseShelf(row=2, column=3, bookcase=bookcases[2], description="Div languages"), + BookcaseShelf(row=3, column=3, bookcase=bookcases[2], description="Python"), + BookcaseShelf(row=4, column=3, bookcase=bookcases[2], description="D&D Minis"), + BookcaseShelf(row=5, column=3, bookcase=bookcases[2], description="Perl"), + + BookcaseShelf(row=0, column=4, bookcase=bookcases[2]), + BookcaseShelf(row=1, column=4, bookcase=bookcases[2], description="Knuth on programming"), + BookcaseShelf(row=2, column=4, bookcase=bookcases[2], description="Div languages"), + BookcaseShelf(row=3, column=4, bookcase=bookcases[2], description="Typesetting"), + BookcaseShelf(row=4, column=4, bookcase=bookcases[2]), + + BookcaseShelf(row=0, column=0, bookcase=bookcases[3]), + BookcaseShelf(row=0, column=1, bookcase=bookcases[3]), + BookcaseShelf(row=0, column=2, bookcase=bookcases[3]), + BookcaseShelf(row=0, column=3, bookcase=bookcases[3]), + BookcaseShelf(row=0, column=4, bookcase=bookcases[3]), + + BookcaseShelf(row=0, column=0, bookcase=bookcases[4]), + BookcaseShelf(row=0, column=1, bookcase=bookcases[4]), + BookcaseShelf(row=0, column=2, bookcase=bookcases[4]), + BookcaseShelf(row=0, column=3, bookcase=bookcases[4]), + BookcaseShelf(row=0, column=4, bookcase=bookcases[4], description="Religion"), ] with open(Path(__file__).parent.parent / 'data' / 'iso639_1.csv') as f: @@ -36,7 +121,7 @@ def seed_data(db: SQLAlchemy): db.session.add_all(media_types) db.session.add_all(bookcases) - db.session.add_all(locations) + db.session.add_all(shelfs) db.session.add_all(languages) db.session.commit() - print("Added test media types, bookcases and locations.") \ No newline at end of file + print("Added test media types, bookcases and shelfs.") \ No newline at end of file diff --git a/worblehat/services/bookcase_item.py b/worblehat/services/bookcase_item.py new file mode 100644 index 0000000..1e96e39 --- /dev/null +++ b/worblehat/services/bookcase_item.py @@ -0,0 +1,45 @@ +import isbnlib + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from ..models import ( + Author, + BookcaseItem, + Language, +) + +def is_valid_pvv_isbn(isbn: str) -> bool: + try: + int(isbn) + except ValueError: + return False + return len(isbn) == 8 + +def is_valid_isbn(isbn: str) -> bool: + return any([ + isbnlib.is_isbn10(isbn), + isbnlib.is_isbn13(isbn), + ]) + +def create_bookcase_item_from_isbn(isbn: str, sql_session: Session) -> BookcaseItem | None: + metadata = isbnlib.meta(isbn, 'openl') + if len(metadata.keys()) == 0: + return None + + bookcase_item = BookcaseItem( + name = metadata.get('Title'), + isbn = int(isbn.replace('-', '')), + ) + + if len(authors := metadata.get('Authors')) > 0: + for author in authors: + bookcase_item.authors.add(Author(author)) + + if (language := metadata.get('Language')): + bookcase_item.language = sql_session.scalars( + select(Language) + .where(Language.iso639_1_code == language) + ).one() + + return bookcase_item \ No newline at end of file diff --git a/worblehat/services/item.py b/worblehat/services/item.py deleted file mode 100644 index a7862c0..0000000 --- a/worblehat/services/item.py +++ /dev/null @@ -1,31 +0,0 @@ -import isbnlib - -from ..models import ( - Author, - BookcaseItem, -) - -def is_valid_isbn(isbn: str) -> bool: - return any([ - isbnlib.is_isbn10(isbn), - isbnlib.is_isbn13(isbn), - ]) - -def create_bookcase_item_from_isbn(isbn: str) -> BookcaseItem | None: - metadata = isbnlib.meta(isbn) - if len(metadata.keys()) == 0: - return None - - bookcase_item = BookcaseItem( - name = metadata.get('Title'), - isbn = int(isbn.replace('-', '')), - ) - - if len(authors := metadata.get('Authors')) > 0: - for author in authors: - bookcase_item.authors.add(Author(author)) - - if (language := metadata.get('language')): - bookcase_item.language = language - - return bookcase_item \ No newline at end of file diff --git a/worblehat/tools/scanner.py b/worblehat/tools/scanner.py index 8db2243..e4444e8 100644 --- a/worblehat/tools/scanner.py +++ b/worblehat/tools/scanner.py @@ -1,5 +1,6 @@ from cmd import Cmd -from typing import Any, Set +from typing import Any +from textwrap import dedent from sqlalchemy import ( create_engine, @@ -9,7 +10,7 @@ from sqlalchemy.orm import ( Session, ) -from worblehat.services.item import ( +from worblehat.services.bookcase_item import ( create_bookcase_item_from_isbn, is_valid_isbn, ) @@ -43,13 +44,11 @@ class _InteractiveItemSelector(Cmd): self, cls: type, sql_session: Session, - items: Set[Any], default: Any | None = None, ): super().__init__() self.cls = cls self.sql_session = sql_session - self.items = items self.default_item = default if default is not None: self.prompt = f'Select {cls.__name__} [{default.name}]> ' @@ -61,13 +60,10 @@ class _InteractiveItemSelector(Cmd): self.result = self.default_item return True - if self.sql_session is not None: - result = self.sql_session.scalars( - select(self.cls) - .where(self.cls.name == arg), - ).all() - else: - result = [x for x in self.items if x.name == arg] + result = self.sql_session.scalars( + select(self.cls) + .where(self.cls.name == arg), + ).all() if len(result) != 1: print(f'No such {self.cls.__name__} found: {arg}') @@ -77,13 +73,10 @@ class _InteractiveItemSelector(Cmd): return True def completenames(self, text: str, *_) -> list[str]: - if self.sql_session is not None: - return self.sql_session.scalars( - select(self.cls.name) - .where(self.cls.name.like(f'{text}%')) - ).all() - else: - return [x for x in self.items if x.name.startswith(text)] + return self.sql_session.scalars( + select(self.cls.name) + .where(self.cls.name.like(f'{text}%')), + ).all() class BookScanTool(Cmd): @@ -94,13 +87,6 @@ Start by entering a command, or entering an ISBN of a new item. Type "help" to see list of commands """ - sql_session = None - - bookcases: set[Bookcase] = set() - bookcase_locations: set[BookcaseLocation] = set() - bookcase_items: set[BookcaseItem] = set() - media_types: set[MediaType] = set() - def __init__(self): super().__init__() @@ -108,32 +94,39 @@ Type "help" to see list of commands engine = create_engine(Config.SQLALCHEMY_DATABASE_URI) self.sql_session = Session(engine) except Exception: - print('Warning: could not connect to database. Saving to database has been disabled.') - return + print('Error: could not connect to database.') + exit(1) - try: - self.bookcases = set(self.sql_session.scalars(select(Bookcase)).all()) - self.bookcase_locations = set(self.sql_session.scalars(select(BookcaseLocation)).all()) - self.bookcase_items = set(self.sql_session.scalars(select(BookcaseItem)).all()) - self.media_types = set(self.sql_session.scalars(select(MediaType)).all()) - except Exception as e: - print(e) - print('Warning: could not prefill data from sql database. Saving to database has been disabled.') - self.sql_session.close() - self.sql_session = None - return - - print('Note: Successfully connected to database') + print(f"Connected to database at '{Config.SQLALCHEMY_DATABASE_URI}'") - def do_list_bookcases(self, arg: str): - self.columnize([repr(bc) for bc in self.bookcases]) + def do_list_bookcases(self, _: str): + """Usage: list_bookcases""" + bookcase_shelfs = self.sql_session.scalars( + select(BookcaseShelf) + .join(Bookcase) + .order_by( + Bookcase.name, + BookcaseShelf.column, + BookcaseShelf.row, + ) + ).all() + + bookcase_uid = None + for shelf in bookcase_shelfs: + if shelf.bookcase.uid != bookcase_uid: + print(shelf.bookcase.name) + bookcase_uid = shelf.bookcase.uid + + name = f"r{shelf.row}-c{shelf.column}" + if shelf.description is not None: + name += f" [{shelf.description}]" + + print(f' {name} - {sum(i.amount for i in shelf.items)} items') def do_add_bookcase(self, arg: str): - """ - Usage: add_bookcase [description] - """ + """Usage: add_bookcase [description]""" arg = arg.split(' ') if len(arg) < 1: print('Usage: add_bookcase [description]') @@ -142,82 +135,105 @@ Type "help" to see list of commands name = arg[0] description = ' '.join(arg[1:]) - if any([bc.name == name for bc in self.bookcases]): + if self.sql_session.scalars( + select(Bookcase) + .where(Bookcase.name == name) + ).one_or_none() is not None: print(f'Error: a bookcase with name {name} already exists') return bookcase = Bookcase(name, description) - self.bookcases.add(bookcase) - if self.sql_session is not None: - self.sql_session.add(bookcase) + self.sql_session.add(bookcase) - - def do_list_bookcase_locations(self, arg: str): - self.columnize([repr(bl) for bl in self.bookcase_locations]) - - - def do_add_bookcase_location(self, arg: str): - """ - Usage: add_bookcase_location [description] - """ + def do_add_bookcase_shelf(self, arg: str): + """Usage: add_bookcase_shelf - [description]""" arg = arg.split(' ') if len(arg) < 2: - print('Usage: add_bookcase_location [description]') + print('Usage: add_bookcase_shelf - [description]') return bookcase_name = arg[0] - name = arg[1] + row, column = [int(x) for x in arg[1].split('-')] description = ' '.join(arg[2:]) - bookcases_with_name = [bc for bc in self.bookcases if bc.name == bookcase_name] - if not len(bookcases_with_name) == 1: + if (bookcase := self.sql_session.scalars( + select(Bookcase) + .where(Bookcase.name == bookcase_name) + ).one_or_none()) is None: print(f'Error: Could not find bookcase with name {bookcase_name}') return - if any([bc.name == name for bc in self.bookcase_locations]): - print(f'Error: a bookcase with name {name} already exists') + if self.sql_session.scalars( + select(BookcaseShelf) + .where( + BookcaseShelf.bookcase == bookcase, + BookcaseShelf.row == row, + BookcaseShelf.column == column, + ) + ).one_or_none() is not None: + print(f'Error: a bookshelf in bookcase {bookcase.name} with position {row}-{column} already exists') return - location = BookcaseLocation( - name, - bookcases_with_name[0], + shelf = BookcaseShelf( + row, + column, + bookcase, description, ) - self.bookcase_locations.add(location) - if self.sql_session is not None: - self.sql_session.add(location) + self.sql_session.add(shelf) - def do_list_bookcase_items(self, arg: str): + def do_list_bookcase_items(self, _: str): + """Usage: list_bookcase_items""" self.columnize([repr(bi) for bi in self.bookcase_items]) - def default(self, arg: str): - if not is_valid_isbn(arg): - print(f'"{arg}" is not a valid isbn') + def default(self, isbn: str): + isbn = isbn.strip() + if not is_valid_isbn(isbn): + print(f'"{isbn}" is not a valid isbn') return - bookcase_item = create_bookcase_item_from_isbn(arg) - if bookcase_item == None: - print(f'Could not find data about item with isbn {arg} online.') - print(f'If you think this is not due to a bug, please add the book to openlibrary.org before continuing.') + if (existing_item := self.sql_session.scalars( + select(BookcaseItem) + .where(BookcaseItem.isbn == isbn) + ).one_or_none()) is not None: + print('Found existing BookcaseItem:', existing_item) + print(f'There are currently {existing_item.amount} of these in the system.') + if _prompt_yes_no('Would you like to add another?', default=True): + existing_item.amount += 1 + return - print('Please select the location where the item is placed:') - bookcase_location_selector = _InteractiveItemSelector( - cls = BookcaseLocation, + bookcase_item = create_bookcase_item_from_isbn(isbn, self.sql_session) + if bookcase_item is None: + print(f'Could not find data about item with isbn {isbn} online.') + print(f'If you think this is not due to a bug, please add the book to openlibrary.org before continuing.') + return + else: + print(dedent(f""" + Found item: + title: {bookcase_item.name} + authors: {', '.join(a.name for a in bookcase_item.authors)} + language: {bookcase_item.language} + """)) + + print('Please select the shelf where the item is placed:') + bookcase_shelf_selector = _InteractiveItemSelector( + cls = BookcaseShelf, sql_session = self.sql_session, - items = self.bookcase_locations, ) - bookcase_location_selector.cmdloop() - bookcase_item.location = bookcase_location_selector.result + bookcase_shelf_selector.cmdloop() + bookcase_item.shelf = bookcase_shelf_selector.result print('Please select the items media type:') media_type_selector = _InteractiveItemSelector( cls = MediaType, sql_session = self.sql_session, - items = self.media_types, - default = next(mt for mt in self.media_types if mt.name.lower() == 'book'), + default = self.sql_session.scalars( + select(MediaType) + .where(MediaType.name.ilike("book")), + ).one(), ) media_type_selector.cmdloop() @@ -227,14 +243,16 @@ Type "help" to see list of commands if username != '': bookcase_item.owner = username - self.bookcase_items.add(bookcase_item) - if self.sql_session is not None: - self.sql_session.add(bookcase_item) + self.sql_session.add(bookcase_item) - def do_exit(self, arg: str): - # TODO: take internally stored data, and save it, either in csv, json or database - raise NotImplementedError() + def do_exit(self, _: str): + """Usage: exit""" + if _prompt_yes_no('Would you like to save your changes?'): + self.sql_session.commit() + else: + self.sql_session.rollback() + exit(0) def main():