diff --git a/.gitignore b/.gitignore index a302af2..b89f631 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ test.php .env **/__pycache__ database.db* -.idea \ No newline at end of file +.idea +db_fixtures \ No newline at end of file diff --git a/src/meal_manager/alembic.ini b/alembic.ini similarity index 100% rename from src/meal_manager/alembic.ini rename to alembic.ini diff --git a/justfile b/justfile new file mode 100644 index 0000000..a154545 --- /dev/null +++ b/justfile @@ -0,0 +1,9 @@ +lint: + uv run isort src + uv run black src + +reset_db: + rm -f database.db + touch database.db + alembic upgrade heads + bash -c 'shopt -s nullglob; for file in db_fixtures/*.sql; do sqlite3 database.db < "$file"; done' \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 74d382d..6794787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dev = [ ] [project.scripts] -apply-subscriptions = "meal_manager.scripts:apply_subscriptions" +apply-subscriptions = "meal_manager.scripts:apply_subscriptions_cli" [tool.isort] @@ -36,7 +36,7 @@ profile = "black" # this is typically a path given in POSIX (e.g. forward slashes) # format, relative to the token %(here)s which refers to the location of this # ini file -script_location = "%(here)s/alembic" +script_location = "%(here)s/src/meal_manager/alembic" # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time diff --git a/src/meal_manager/alembic/versions/2025_10_12_2046-299a83240036_inital_revision.py b/src/meal_manager/alembic/versions/2025_10_12_2046-299a83240036_inital_revision.py index 1718dda..ceb0bb9 100644 --- a/src/meal_manager/alembic/versions/2025_10_12_2046-299a83240036_inital_revision.py +++ b/src/meal_manager/alembic/versions/2025_10_12_2046-299a83240036_inital_revision.py @@ -84,7 +84,7 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("household_id"), ) op.create_table( - "team_registration", + "teamregistration", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), sa.Column("event_id", sa.Integer(), nullable=False), sa.Column("person_name", sa.String(), nullable=False), diff --git a/src/meal_manager/alembic/versions/2025_10_27_1225-13084c5c1f68_add_subscriptions_applied_column_to_.py b/src/meal_manager/alembic/versions/2025_10_27_1225-13084c5c1f68_add_subscriptions_applied_column_to_.py new file mode 100644 index 0000000..cabc65f --- /dev/null +++ b/src/meal_manager/alembic/versions/2025_10_27_1225-13084c5c1f68_add_subscriptions_applied_column_to_.py @@ -0,0 +1,34 @@ +"""Add subscriptions_applied column to Event + +Revision ID: 13084c5c1f68 +Revises: 299a83240036 +Create Date: 2025-10-27 12:25:14.633641 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "13084c5c1f68" +down_revision: Union[str, Sequence[str], None] = "299a83240036" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "event", sa.Column("subscriptions_applied", sa.Boolean(), nullable=False) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("event", "subscriptions_applied") + # ### end Alembic commands ### diff --git a/src/meal_manager/models.py b/src/meal_manager/models.py index 38ceec9..046baa1 100644 --- a/src/meal_manager/models.py +++ b/src/meal_manager/models.py @@ -33,6 +33,8 @@ class Event(Base): team_prep_min: Mapped[int] = mapped_column(default=1, nullable=False) team_prep_max: Mapped[int] = mapped_column(default=1, nullable=False) + subscriptions_applied: Mapped[bool] = mapped_column(default=False, nullable=False) + registrations: Mapped[list["Registration"]] = relationship( "Registration", cascade="all, delete" ) diff --git a/src/meal_manager/scripts.py b/src/meal_manager/scripts.py index b663181..c1b8bb6 100644 --- a/src/meal_manager/scripts.py +++ b/src/meal_manager/scripts.py @@ -1,33 +1,38 @@ import argparse +import datetime -from sqlalchemy import select +from sqlalchemy import Date, cast, func, select from sqlalchemy.orm import Session from meal_manager.main import engine from meal_manager.models import Event, Registration, Subscription -def apply_subscriptions(): - parser = argparse.ArgumentParser(description="Apply subscriptions for an event") - parser.add_argument("event_id", type=int, help="Event ID (required)") - parser.add_argument( - "--dry-run", action="store_true", help="Run without making changes" - ) +def apply_subscriptions(session: Session, event: Event = None, dry_run: bool = False): - args = parser.parse_args() + subscriptions = session.scalars(select(Subscription)).all() - # Access the arguments - event_id = args.event_id - dry_run = args.dry_run + if event is not None: + events = [event] + else: + today = datetime.date.today() + query = select(Event).where( + ~Event.subscriptions_applied, + func.strftime("%Y-%m-%d %H:%M:%S", Event.event_time) >= today.isoformat(), + func.strftime("%Y-%m-%d %H:%M:%S", Event.event_time) + <= (today + datetime.timedelta(days=7)).isoformat(), + ) + events = session.scalars(query).all() - with Session(engine) as session: - subscriptions = session.scalars(select(Subscription)).all() - event = session.scalars(select(Event).where(Event.id == event_id)).one() + if len(events) == 0: + print("No events to process") + return + for event in events: if dry_run: - print(f"DRY RUN: Would process event {event_id}") + print(f"DRY RUN: Would process event {event.title} ({event.id})") else: - print(f"Processing event {event_id}") + print(f"Processing event {event.title} ({event.id})") print(f"There are {len(subscriptions)} subscriptions to process") relevant_subscriptions = [ @@ -60,6 +65,28 @@ def apply_subscriptions(): else: session.add(reg) print(f"Registered {subscription.household.name}") + event.subscriptions_applied = True - if not dry_run: - session.commit() + if not dry_run: + session.commit() + + +def apply_subscriptions_cli(): + parser = argparse.ArgumentParser(description="Apply subscriptions for an event") + parser.add_argument("--event_id", type=int, help="Event ID (required)") + parser.add_argument( + "--dry-run", action="store_true", help="Run without making changes" + ) + + args = parser.parse_args() + + # Access the arguments + event_id = args.event_id + dry_run = args.dry_run + + with Session(engine) as session: + if event_id is not None: + event = session.scalars(select(Event).where(Event.id == event_id)).one() + apply_subscriptions(session, event, dry_run) + else: + apply_subscriptions(session, dry_run=dry_run)