diff --git a/alembic/versions/20241127_b1b08faa9811_update_database_nullable_constraints.py b/alembic/versions/20241127_9dafb82f1fac_update_database_nullable_constraints.py similarity index 72% rename from alembic/versions/20241127_b1b08faa9811_update_database_nullable_constraints.py rename to alembic/versions/20241127_9dafb82f1fac_update_database_nullable_constraints.py index 72a355938..87e73d94d 100644 --- a/alembic/versions/20241127_b1b08faa9811_update_database_nullable_constraints.py +++ b/alembic/versions/20241127_9dafb82f1fac_update_database_nullable_constraints.py @@ -1,8 +1,8 @@ """Update database nullable constraints -Revision ID: b1b08faa9811 +Revision ID: 9dafb82f1fac Revises: 272da5f400de -Create Date: 2024-11-27 17:05:00.839954+00:00 +Create Date: 2024-11-27 17:21:28.727991+00:00 """ @@ -10,7 +10,7 @@ from alembic import op # revision identifiers, used by Alembic. -revision = "b1b08faa9811" +revision = "9dafb82f1fac" down_revision = "272da5f400de" branch_labels = None depends_on = None @@ -37,12 +37,28 @@ def upgrade() -> None: op.alter_column( "holds", "license_pool_id", existing_type=sa.INTEGER(), nullable=False ) + op.alter_column("lanes", "display_name", existing_type=sa.VARCHAR(), nullable=False) op.alter_column( "licensepools", "data_source_id", existing_type=sa.INTEGER(), nullable=False ) op.alter_column( "licensepools", "identifier_id", existing_type=sa.INTEGER(), nullable=False ) + op.alter_column( + "licensepools", "licenses_owned", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "licensepools", "licenses_available", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "licensepools", "licenses_reserved", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "licensepools", + "patrons_in_hold_queue", + existing_type=sa.INTEGER(), + nullable=False, + ) op.alter_column( "licenses", "license_pool_id", existing_type=sa.INTEGER(), nullable=False ) @@ -68,12 +84,28 @@ def downgrade() -> None: op.alter_column( "licenses", "license_pool_id", existing_type=sa.INTEGER(), nullable=True ) + op.alter_column( + "licensepools", + "patrons_in_hold_queue", + existing_type=sa.INTEGER(), + nullable=True, + ) + op.alter_column( + "licensepools", "licenses_reserved", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "licensepools", "licenses_available", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "licensepools", "licenses_owned", existing_type=sa.INTEGER(), nullable=True + ) op.alter_column( "licensepools", "identifier_id", existing_type=sa.INTEGER(), nullable=True ) op.alter_column( "licensepools", "data_source_id", existing_type=sa.INTEGER(), nullable=True ) + op.alter_column("lanes", "display_name", existing_type=sa.VARCHAR(), nullable=True) op.alter_column( "holds", "license_pool_id", existing_type=sa.INTEGER(), nullable=True ) diff --git a/src/palace/manager/scripts/configuration.py b/src/palace/manager/scripts/configuration.py index f9dd92500..008299f87 100644 --- a/src/palace/manager/scripts/configuration.py +++ b/src/palace/manager/scripts/configuration.py @@ -278,13 +278,17 @@ def do_run(self, _db=None, cmd_args=None, output=sys.stdout): id = args.id lane = get_one(_db, Lane, id=id) if not lane: - if args.library_short_name: + if args.library_short_name and args.display_name: library = get_one(_db, Library, short_name=args.library_short_name) if not library: raise ValueError('No such library: "%s".' % args.library_short_name) - lane, is_new = create(_db, Lane, library=library) + lane, is_new = create( + _db, Lane, library=library, display_name=args.display_name + ) else: - raise ValueError("Library short name is required to create a new lane.") + raise ValueError( + "Library short name and lane display name are required to create a new lane." + ) if args.parent_id: lane.parent_id = args.parent_id diff --git a/src/palace/manager/sqlalchemy/model/collection.py b/src/palace/manager/sqlalchemy/model/collection.py index f9cf8d384..92d00e69b 100644 --- a/src/palace/manager/sqlalchemy/model/collection.py +++ b/src/palace/manager/sqlalchemy/model/collection.py @@ -87,7 +87,7 @@ class Collection(Base, HasSessionCache, RedisKeyMixin): # secret as the Overdrive collection, but it has a distinct # external_account_id. parent_id = Column(Integer, ForeignKey("collections.id"), index=True) - parent: Collection = relationship( + parent: Mapped[Collection | None] = relationship( "Collection", remote_side=[id], back_populates="children" ) diff --git a/src/palace/manager/sqlalchemy/model/lane.py b/src/palace/manager/sqlalchemy/model/lane.py index 24ef9602b..d50f5f06f 100644 --- a/src/palace/manager/sqlalchemy/model/lane.py +++ b/src/palace/manager/sqlalchemy/model/lane.py @@ -2686,7 +2686,7 @@ class Lane(Base, DatabaseBackedWorkList, HierarchyWorkList): # okay for this to be duplicated within a library, but it's not # okay to have two lanes with the same parent and the same display # name -- that would be confusing. - display_name: str = Column(Unicode) + display_name: Mapped[str] = Column(Unicode, nullable=False) # True = Fiction only # False = Nonfiction only diff --git a/src/palace/manager/sqlalchemy/model/library.py b/src/palace/manager/sqlalchemy/model/library.py index b8bf5f35e..22e6e5c5b 100644 --- a/src/palace/manager/sqlalchemy/model/library.py +++ b/src/palace/manager/sqlalchemy/model/library.py @@ -120,7 +120,7 @@ class Library(Base, HasSessionCache): ) # Any additional configuration information is stored as JSON on this column. - settings_dict: dict[str, Any] = Column(JSONB, nullable=False, default=dict) + settings_dict: Mapped[dict[str, Any]] = Column(JSONB, nullable=False, default=dict) # A Library may have many CirculationEvents circulation_events: Mapped[list[CirculationEvent]] = relationship( diff --git a/src/palace/manager/sqlalchemy/model/licensing.py b/src/palace/manager/sqlalchemy/model/licensing.py index d15a34d63..ed45705a6 100644 --- a/src/palace/manager/sqlalchemy/model/licensing.py +++ b/src/palace/manager/sqlalchemy/model/licensing.py @@ -300,10 +300,12 @@ class LicensePool(Base): open_access = Column(Boolean, index=True) last_checked = Column(DateTime(timezone=True), index=True) - licenses_owned: int = Column(Integer, default=0, index=True) - licenses_available: int = Column(Integer, default=0, index=True) - licenses_reserved: int = Column(Integer, default=0) - patrons_in_hold_queue: int = Column(Integer, default=0) + licenses_owned: Mapped[int] = Column(Integer, default=0, index=True, nullable=False) + licenses_available: Mapped[int] = Column( + Integer, default=0, index=True, nullable=False + ) + licenses_reserved: Mapped[int] = Column(Integer, default=0, nullable=False) + patrons_in_hold_queue: Mapped[int] = Column(Integer, default=0, nullable=False) should_track_playtime: Mapped[bool] = Column(Boolean, default=False, nullable=False) # This lets us cache the work of figuring out the best open access diff --git a/tests/manager/api/admin/controller/test_custom_lists.py b/tests/manager/api/admin/controller/test_custom_lists.py index 59a7023b1..8c5b54e82 100644 --- a/tests/manager/api/admin/controller/test_custom_lists.py +++ b/tests/manager/api/admin/controller/test_custom_lists.py @@ -1054,12 +1054,8 @@ def test_share_locally_delete(self, admin_librarian_fixture: AdminLibrarianFixtu # First, we are shared with a library which uses the list # so we cannot delete the share status - lane_with_shared, _ = create( - admin_librarian_fixture.ctrl.db.session, - Lane, - library_id=s.shared_with.id, - customlists=[s.list], - ) + lane_with_shared = admin_librarian_fixture.ctrl.db.lane(library=s.shared_with) + lane_with_shared.customlists = [s.list] with admin_librarian_fixture.request_context_with_library_and_admin( "/", method="DELETE", library=s.primary_library @@ -1090,12 +1086,10 @@ def test_share_locally_delete(self, admin_librarian_fixture: AdminLibrarianFixtu resp = self._share_locally(s.list, s.primary_library, admin_librarian_fixture) assert resp["successes"] == 1 - lane_with_primary, _ = create( - admin_librarian_fixture.ctrl.db.session, - Lane, - library_id=s.primary_library.id, - customlists=[s.list], + lane_with_primary = admin_librarian_fixture.ctrl.db.lane( + library=s.primary_library, ) + lane_with_primary.customlists = [s.list] with admin_librarian_fixture.request_context_with_library_and_admin( "/", method="DELETE", library=s.primary_library ):