Fix flexible-wish double-booking, extend calendar cache to 2055, add journey tests
Second real bug found by driving the running app while the suites were green. Flexible wishes were funded twice across propose -> apply -> re-propose. Because planned_leave_day carries no wish_id, the engine could not tell that applied days were that wish's own prior occurrence; it treated them as occupied space, routed the candidate search around them, and funded the year again on shifted dates while reporting weeks_funded 1.0. Applying twice turned "one week each spring" into 18 paid days instead of 9. Fixed wishes were unaffected and stay unaffected. Fix (allocation_service.py), per the approved approach of counting already-applied days inside the bounding range rather than adding a wish_id column: - _Ledger._persisted_lookup snapshots existing days BEFORE this run folds in its own freshly-computed windows, so a fixed wish's new placement is never mistaken for a flexible wish's history - _reuse_already_funded_window finds this wish's persisted dates within its bound and reuses the window containing them, via the ordinary fund_window path - full funding therefore yields zero new days and partial funding tops up exactly the remainder, with no duplication and no false shortfall - with no persisted dates, or if the reused window is no longer collision-free, it falls through to the original search, so an unsatisfied year is never frozen Calendar cache extended 2026-2035 -> 2026-2055 (39KB -> 116KB) and the allocator switched to strict, so an uncached year raises UncachedYearError -> ValidationError -> HTTP 422 instead of silently treating Julafton, Nyarsafton and Midsommarafton as working days. Also fixed normalize_day dropping the second name when two holidays share a date (Nationaldagen was being lost). 2049 and 2055 legitimately have 17 red days, not 18: Pingstdagen coincides with Nationaldagen. New backend/tests/integration/test_user_journey.py drives the real app over HTTP across seven journeys: the double-booking guard, the fixed-wish control, apply idempotency, fixed-never-displaced-by-flexible, flexible wishes tracking Easter per year, age-4 arithmetic self-consistency, and a recursive check that no Swedish text reaches the wire now that everything is message_key + data. New backend/scripts/seed_demo.py seeds a realistic workspace through the real service layer for manual testing: demo@example.com / DemoPass123! Backend 414 -> 435 tests. Frontend unchanged at 705. Still not verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -24,7 +24,10 @@ import httpx
|
||||
# Configuration
|
||||
DAGSMART_API_BASE = "https://api.dagsmart.se/v3/holidays"
|
||||
CURRENT_YEAR = datetime.date.today().year
|
||||
YEARS_TO_FETCH = 10 # Fetch 10 years forward from current year (inclusive)
|
||||
YEARS_TO_FETCH = 30 # Fetch 30 years forward from current year (inclusive).
|
||||
# Bump this constant (and rerun this script) whenever the planning horizon
|
||||
# risks approaching the end of the cached range; see AllocationInputLoader's
|
||||
# strict=True check in app/services/allocation_service.py.
|
||||
RATE_LIMIT_DELAY = 2.5 # 5 requests per 10 seconds; conservative margin
|
||||
OUTPUT_DIR = Path(__file__).parent.parent / "app" / "data"
|
||||
OUTPUT_FILE = OUTPUT_DIR / "swedish_calendar.json"
|
||||
@@ -102,22 +105,32 @@ def normalize_day(day: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
Normalizes category to consistent "de_facto" spelling and lowercases names.
|
||||
|
||||
A date can carry more than one `observances` entry when two distinct
|
||||
holidays coincide on the same calendar date (e.g. Pingstdagen and
|
||||
Sveriges nationaldag both land on 2049-06-06 and 2055-06-06, since
|
||||
Pentecost is Easter + 49 days and National Day is fixed at June 6). All
|
||||
observance names are joined with "; " — mirroring the same convention
|
||||
`app/utils/holidays.py` already uses for a holiday that coincides with a
|
||||
Sunday — so a date-collision year never silently loses one of the two
|
||||
names the way taking only `observances[0]` would.
|
||||
|
||||
Args:
|
||||
day: Day record from Dagsmart API response
|
||||
|
||||
Returns:
|
||||
Normalized day record with our schema
|
||||
"""
|
||||
# Extract name from the first observance
|
||||
# Join every observance's name (usually just one) so a same-date
|
||||
# collision between two distinct holidays never drops one silently.
|
||||
observances = day.get("observances", [])
|
||||
name_sv = "Unknown"
|
||||
name_en = "Unknown"
|
||||
|
||||
if observances:
|
||||
primary_observance = observances[0]
|
||||
name_data = primary_observance.get("name", {})
|
||||
name_sv = name_data.get("sv", "Unknown")
|
||||
name_en = name_data.get("en", "Unknown")
|
||||
sv_names = [o.get("name", {}).get("sv", "") for o in observances]
|
||||
en_names = [o.get("name", {}).get("en", "") for o in observances]
|
||||
name_sv = "; ".join(n for n in sv_names if n) or "Unknown"
|
||||
name_en = "; ".join(n for n in en_names if n) or "Unknown"
|
||||
|
||||
# Normalize category: vendor uses "defacto", we use "de_facto"
|
||||
category = day["holidayType"]
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed a realistic, ready-to-explore demo workspace for manual/browser testing.
|
||||
|
||||
Everything below goes through the normal service/repository layers — the same
|
||||
`AuthService`, `WorkspaceService`, `ChildService`, `VacationDaysService`,
|
||||
`DayOverrideService`, `LeaveWishService`, `ManualFetchService` and
|
||||
`PlanningService` the API routers call — so it exercises real validation and
|
||||
real code paths instead of poking SQLite directly. It creates:
|
||||
|
||||
1. A user with fixed, printed credentials (see DEMO_EMAIL/DEMO_PASSWORD)
|
||||
so a human can just log in at http://localhost:5173.
|
||||
2. A couple workspace with a partner (a login-less PersonalData profile,
|
||||
via `WorkspaceService.add_placeholder_parent` — "solo planning for a
|
||||
partner who hasn't registered", see PersonalData's docstring) so
|
||||
shared/partner planning (reserved days, dubbeldagar) is visible.
|
||||
3. A child born ~6 months before the day the script is first run, so the
|
||||
age-4 boundary and the whole planning horizon sit a few years out
|
||||
rather than silently aging out of a hardcoded year.
|
||||
4. Vacation-days rows for only some years, so `is_assumed` (§3.2) shows up
|
||||
on the years intentionally left unset.
|
||||
5. Day overrides: one WORKED override on a date the calendar treats as a
|
||||
holiday, and one OFF override on Valborgsmässoafton (30 April) — which
|
||||
§6 says is deliberately NOT a default day off, making it the natural
|
||||
"we take it off anyway" example.
|
||||
6. Four leave wishes: two fixed recurring ("Sommar", "Jul", the latter
|
||||
wrapping New Year's), one flexible spring wish bounded 03-01..05-31
|
||||
(exercises the flexible/Easter-picking resolution path, §10.2), and one
|
||||
low-priority fixed wish pinned to the demo user specifically, whose FP
|
||||
pool and that year's vacation are deliberately drained so a real
|
||||
shortfall (and its remedies) show up in the very first proposal.
|
||||
7. A handful of already-applied planned days for the partner (who is left
|
||||
with a large surplus pool) so the Dashboard/Kalender are not empty on
|
||||
first load, and so `surplus_advice` (§9.2) has something to point at.
|
||||
|
||||
Idempotent / re-runnable: if the demo user already exists, the script prints
|
||||
the existing summary and exits without touching the database — never
|
||||
destructive by default. Pass --reset to delete the existing demo workspace
|
||||
(and only that workspace, cascading via FK ON DELETE CASCADE — see
|
||||
`app/db/connection.py`) and recreate it from scratch.
|
||||
|
||||
Usage (from backend/):
|
||||
uv run python scripts/seed_demo.py # create once, or report existing
|
||||
uv run python scripts/seed_demo.py --reset # wipe the demo workspace and rebuild
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from app.config import settings
|
||||
from app.db.connection import connect, run_migrations
|
||||
from app.entities.personal_data import PersonalData
|
||||
from app.entities.user import User
|
||||
from app.repositories_impl.sqlite.child_repository import SQLiteChildRepository
|
||||
from app.repositories_impl.sqlite.day_override_repository import SQLiteDayOverrideRepository
|
||||
from app.repositories_impl.sqlite.leave_wish_repository import SQLiteLeaveWishRepository
|
||||
from app.repositories_impl.sqlite.parental_leave_repository import SQLiteParentalLeaveRepository
|
||||
from app.repositories_impl.sqlite.personal_data_repository import SQLitePersonalDataRepository
|
||||
from app.repositories_impl.sqlite.planned_leave_repository import SQLitePlannedLeaveRepository
|
||||
from app.repositories_impl.sqlite.user_repository import SQLiteUserRepository
|
||||
from app.repositories_impl.sqlite.vacation_days_repository import SQLiteVacationDaysRepository
|
||||
from app.repositories_impl.sqlite.workspace_repository import (
|
||||
SQLiteWorkspaceInvitationRepository,
|
||||
SQLiteWorkspaceRepository,
|
||||
)
|
||||
from app.services.analytics_service import AnalyticsService
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.child_service import ChildService
|
||||
from app.services.day_override_service import DayOverrideService
|
||||
from app.services.fetch_data_service import ManualFetchService
|
||||
from app.services.leave_wish_service import LeaveWishService
|
||||
from app.services.planning_service import PlanningService
|
||||
from app.services.vacation_days_service import VacationDaysService
|
||||
from app.services.workspace_service import WorkspaceService
|
||||
from app.utils.holidays import SUNDAY_HOLIDAY_NAME, get_swedish_holidays
|
||||
|
||||
DEMO_EMAIL = "demo@example.com"
|
||||
DEMO_PASSWORD = "DemoPass123!" # dev-only, printed below — never a real secret
|
||||
DEMO_NAME = "Alex"
|
||||
PARTNER_NAME = "Sam"
|
||||
CHILD_NAME = "Nova"
|
||||
FRONTEND_URL = "http://localhost:5173"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DemoServices:
|
||||
"""Every service the seeder needs, wired the same way `dependencies.py` does."""
|
||||
|
||||
auth: AuthService
|
||||
workspace: WorkspaceService
|
||||
child: ChildService
|
||||
vacation: VacationDaysService
|
||||
day_override: DayOverrideService
|
||||
leave_wish: LeaveWishService
|
||||
manual_fetch: ManualFetchService
|
||||
planning: PlanningService
|
||||
users: SQLiteUserRepository
|
||||
personal_data: SQLitePersonalDataRepository
|
||||
workspaces: SQLiteWorkspaceRepository
|
||||
|
||||
|
||||
def _build_services(conn: aiosqlite.Connection) -> DemoServices:
|
||||
users = SQLiteUserRepository(conn)
|
||||
personal_data = SQLitePersonalDataRepository(conn)
|
||||
workspaces = SQLiteWorkspaceRepository(conn)
|
||||
invitations = SQLiteWorkspaceInvitationRepository(conn)
|
||||
children = SQLiteChildRepository(conn)
|
||||
vacation_repo = SQLiteVacationDaysRepository(conn)
|
||||
day_overrides = SQLiteDayOverrideRepository(conn)
|
||||
leave_wishes = SQLiteLeaveWishRepository(conn)
|
||||
parental_leave = SQLiteParentalLeaveRepository(conn)
|
||||
planned_leave = SQLitePlannedLeaveRepository(conn)
|
||||
|
||||
analytics = AnalyticsService(parental_leave, planned_leave, children, personal_data)
|
||||
|
||||
return DemoServices(
|
||||
auth=AuthService(users, personal_data, workspaces),
|
||||
workspace=WorkspaceService(workspaces, invitations, personal_data),
|
||||
child=ChildService(children),
|
||||
vacation=VacationDaysService(vacation_repo),
|
||||
day_override=DayOverrideService(day_overrides),
|
||||
leave_wish=LeaveWishService(leave_wishes),
|
||||
manual_fetch=ManualFetchService(parental_leave),
|
||||
planning=PlanningService(planned_leave, analytics, parental_leave),
|
||||
users=users,
|
||||
personal_data=personal_data,
|
||||
workspaces=workspaces,
|
||||
)
|
||||
|
||||
|
||||
async def _find_existing_demo_user(services: DemoServices) -> User | None:
|
||||
return await services.users.get_by_email(DEMO_EMAIL)
|
||||
|
||||
|
||||
async def _reset(services: DemoServices, user: User) -> None:
|
||||
"""Delete the demo user's workspace, cascading via FK ON DELETE CASCADE.
|
||||
|
||||
Removes everything hanging off it (personal_data, the user row itself,
|
||||
child, leave_wish, vacation_days, planned_leave_day, day_override,
|
||||
parental_leave_fetch, workspace_invitation) — see the CASCADE chain in
|
||||
app/db/connection.py. This never touches any other user's data: it only
|
||||
ever deletes the one workspace resolved from the demo user's own
|
||||
personal_data row.
|
||||
"""
|
||||
pd = await services.personal_data.get_by_id(user.personal_data_uuid)
|
||||
if pd is None:
|
||||
# Orphaned user row (shouldn't happen); nothing to cascade from.
|
||||
return
|
||||
await services.workspaces.delete(pd.workspace_id)
|
||||
print(f"[reset] Deleted existing demo workspace {pd.workspace_id} (cascaded).")
|
||||
|
||||
|
||||
async def _print_existing_summary(services: DemoServices, user: User) -> None:
|
||||
pd = await services.personal_data.get_by_id(user.personal_data_uuid)
|
||||
workspace_id = pd.workspace_id if pd is not None else None
|
||||
print("Demo user already exists — nothing changed. Re-run with --reset to rebuild it.")
|
||||
print(f" Email: {DEMO_EMAIL}")
|
||||
print(f" Password: {DEMO_PASSWORD}")
|
||||
print(f" Workspace id: {workspace_id}")
|
||||
print(f" Open: {FRONTEND_URL}")
|
||||
|
||||
|
||||
def _pick_worked_override_date(birth_year: int, birth_date: date) -> tuple[date, str]:
|
||||
"""Pick a real calendar holiday, on/after the child's birth date, to force-work.
|
||||
|
||||
This is the "we must work despite the calendar" override direction (see
|
||||
DayOverride's docstring), i.e. `is_working_day=True`. Looked up from the
|
||||
actual holiday calendar rather than hardcoded, so it stays correct
|
||||
regardless of which years the horizon covers.
|
||||
"""
|
||||
holidays = get_swedish_holidays(birth_year)
|
||||
holidays.update(get_swedish_holidays(birth_year + 1))
|
||||
# Every Sunday is also in this map (SUNDAY_HOLIDAY_NAME) — skip those, a
|
||||
# named red day/klämdag makes a far more legible override example.
|
||||
candidates = sorted(
|
||||
d for d, name in holidays.items() if d >= birth_date and name != SUNDAY_HOLIDAY_NAME
|
||||
)
|
||||
chosen = candidates[0]
|
||||
return chosen, holidays[chosen]
|
||||
|
||||
|
||||
async def _seed(conn: aiosqlite.Connection, *, reset: bool) -> None:
|
||||
services = _build_services(conn)
|
||||
|
||||
existing = await _find_existing_demo_user(services)
|
||||
if existing is not None:
|
||||
if not reset:
|
||||
await _print_existing_summary(services, existing)
|
||||
return
|
||||
await _reset(services, existing)
|
||||
|
||||
today = date.today()
|
||||
birth_date = today - relativedelta(months=6)
|
||||
birth_year = birth_date.year
|
||||
|
||||
# -- 1. User + personal workspace + PersonalData (AuthService.register) --
|
||||
_user, alex = await services.auth.register(
|
||||
email=DEMO_EMAIL, password=DEMO_PASSWORD, display_name=DEMO_NAME
|
||||
)
|
||||
workspace_id = alex.workspace_id
|
||||
await services.workspace.update_workspace(
|
||||
workspace_id, planning_mode="couple", planning_goal="balanced"
|
||||
)
|
||||
|
||||
# -- 2. Partner (no login — shared/partner planning needs a second parent
|
||||
# on the workspace, but nothing requires them to have registered) -------
|
||||
sam = await services.workspace.add_placeholder_parent(workspace_id, PARTNER_NAME)
|
||||
|
||||
# -- 3. Child, born ~6 months ago relative to today ----------------------
|
||||
child = await services.child.create(
|
||||
workspace_id=workspace_id,
|
||||
name=CHILD_NAME,
|
||||
birth_date=birth_date,
|
||||
preschool_start_date=birth_date + relativedelta(years=1, months=4),
|
||||
preschool_planning_days_per_year=6,
|
||||
)
|
||||
fourth_birthday = birth_date + relativedelta(years=4)
|
||||
|
||||
# -- 4. Vacation-days rows for only SOME years -----------------------
|
||||
# Alex gets birth_year (deliberately 0 — see the manual FP entry below,
|
||||
# this is the year the shortfall demo bites) and birth_year+1 (a normal
|
||||
# entered year, with some carried-over days). Every other year up to the
|
||||
# 4th birthday is left unset on purpose so `is_assumed` shows on them.
|
||||
await services.vacation.upsert(
|
||||
personal_data_uuid=alex.uuid, workspace_id=workspace_id, year=birth_year, total_days=0
|
||||
)
|
||||
await services.vacation.upsert(
|
||||
personal_data_uuid=alex.uuid,
|
||||
workspace_id=workspace_id,
|
||||
year=birth_year + 1,
|
||||
total_days=20,
|
||||
saved_days_in=3,
|
||||
)
|
||||
# Sam gets one entered year too, rest assumed.
|
||||
await services.vacation.upsert(
|
||||
personal_data_uuid=sam.uuid, workspace_id=workspace_id, year=birth_year, total_days=25
|
||||
)
|
||||
|
||||
# -- 5. Manual FP snapshot for Alex, deliberately near-drained ----------
|
||||
# Narrative: Alex front-loaded almost all of their sjukpenning/lägstanivå
|
||||
# days right after birth, before this planner was in use. Combined with
|
||||
# the 0-day vacation year above, this guarantees the low-priority
|
||||
# "Höstlov" wish below cannot be fully funded — a real, visible shortfall
|
||||
# in the very first proposal, not a contrived one.
|
||||
await services.manual_fetch.save_manual_entry(
|
||||
personal_data_uuid=alex.uuid,
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
total_days=240,
|
||||
days_used=240,
|
||||
reserved_days_total=90,
|
||||
reserved_days_left=0,
|
||||
transferable_days_left=0,
|
||||
dubbeldagar_left=55,
|
||||
sjukpenning_total=195,
|
||||
sjukpenning_used=195,
|
||||
lagsta_niva_total=45,
|
||||
lagsta_niva_used=45,
|
||||
)
|
||||
|
||||
# -- 6. Day overrides -----------------------------------------------
|
||||
worked_date, worked_holiday_name = _pick_worked_override_date(birth_year, birth_date)
|
||||
await services.day_override.upsert(
|
||||
workspace_id=workspace_id,
|
||||
day=worked_date,
|
||||
is_working_day=True,
|
||||
note=f"Vi jobbar trots {worked_holiday_name.lower()} i år",
|
||||
)
|
||||
valborg_year = birth_year if date(birth_year, 4, 30) >= birth_date else birth_year + 1
|
||||
await services.day_override.upsert(
|
||||
workspace_id=workspace_id,
|
||||
day=date(valborg_year, 4, 30),
|
||||
is_working_day=False,
|
||||
note="Vi tar ledigt på Valborgsmässoafton",
|
||||
)
|
||||
|
||||
# -- 7. Leave wishes: fixed x2, flexible x1, and the deliberately-tight
|
||||
# low-priority one pinned to Alex --------------------------------------
|
||||
await services.leave_wish.create(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
name="Sommar",
|
||||
window_start_month=6,
|
||||
window_start_day=15,
|
||||
window_end_month=7,
|
||||
window_end_day=26,
|
||||
target_weeks=6,
|
||||
recurrence="yearly",
|
||||
first_year=birth_year,
|
||||
priority=1,
|
||||
parent_mode="either",
|
||||
)
|
||||
await services.leave_wish.create(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
name="Jul",
|
||||
window_start_month=12,
|
||||
window_start_day=20,
|
||||
window_end_month=1,
|
||||
window_end_day=16, # wraps into January
|
||||
target_weeks=4,
|
||||
recurrence="yearly",
|
||||
first_year=birth_year,
|
||||
priority=1,
|
||||
parent_mode="either",
|
||||
)
|
||||
await services.leave_wish.create(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
name="Vår",
|
||||
window_start_month=3,
|
||||
window_start_day=1,
|
||||
window_end_month=5,
|
||||
window_end_day=31,
|
||||
target_weeks=1,
|
||||
recurrence="yearly",
|
||||
first_year=birth_year + 1,
|
||||
priority=2,
|
||||
parent_mode="either",
|
||||
window_mode="flexible",
|
||||
)
|
||||
await services.leave_wish.create(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
name="Höstlov",
|
||||
window_start_month=10,
|
||||
window_start_day=25,
|
||||
window_end_month=11,
|
||||
window_end_day=1,
|
||||
target_weeks=1,
|
||||
recurrence="yearly",
|
||||
first_year=birth_year,
|
||||
priority=3,
|
||||
parent_mode="specific",
|
||||
parent_personal_data_uuid=alex.uuid,
|
||||
)
|
||||
|
||||
# -- 8. A few already-applied planned days for Sam (ample balance) so
|
||||
# the Dashboard/Kalender are not empty on first load --------------------
|
||||
planned_start = today + timedelta(days=7)
|
||||
applied_days = 0
|
||||
for offset, day_type in ((0, "vacation"), (1, "vacation"), (2, "sjukpenning")):
|
||||
await services.planning.add_planned_day(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
parent_personal_data_uuid=sam.uuid,
|
||||
date=planned_start + timedelta(days=offset),
|
||||
day_type=day_type,
|
||||
)
|
||||
applied_days += 1
|
||||
|
||||
_print_summary(
|
||||
workspace_id=workspace_id,
|
||||
child_id=child.id,
|
||||
alex=alex,
|
||||
sam=sam,
|
||||
birth_date=birth_date,
|
||||
fourth_birthday=fourth_birthday,
|
||||
worked_override=(worked_date, worked_holiday_name),
|
||||
valborg_year=valborg_year,
|
||||
applied_days=applied_days,
|
||||
)
|
||||
|
||||
|
||||
def _print_summary(
|
||||
*,
|
||||
workspace_id: UUID,
|
||||
child_id: UUID,
|
||||
alex: PersonalData,
|
||||
sam: PersonalData,
|
||||
birth_date: date,
|
||||
fourth_birthday: date,
|
||||
worked_override: tuple[date, str],
|
||||
valborg_year: int,
|
||||
applied_days: int,
|
||||
) -> None:
|
||||
worked_date, worked_name = worked_override
|
||||
print("=" * 78)
|
||||
print("DEMO WORKSPACE SEEDED")
|
||||
print("=" * 78)
|
||||
print()
|
||||
print("Login credentials:")
|
||||
print(f" Email: {DEMO_EMAIL}")
|
||||
print(f" Password: {DEMO_PASSWORD}")
|
||||
print()
|
||||
print("Created:")
|
||||
print(f" Workspace id: {workspace_id} (planning_mode=couple)")
|
||||
print(f" Parent 1: {DEMO_NAME} ({alex.uuid}) — has a login, near-drained FP pool")
|
||||
print(f" Parent 2: {PARTNER_NAME} ({sam.uuid}) — no login, large surplus pool")
|
||||
print(f" Child: {CHILD_NAME} ({child_id}), born {birth_date.isoformat()}")
|
||||
print(f" 4th birthday {fourth_birthday.isoformat()}")
|
||||
print(" Vacation: entered for 2 years per parent, other years left")
|
||||
print(" unset (is_assumed badge should show on those)")
|
||||
print(
|
||||
f" Overrides: worked {worked_date.isoformat()} ({worked_name}); "
|
||||
f"off {valborg_year}-04-30 (Valborgsmässoafton)"
|
||||
)
|
||||
print(" Wishes: Sommar (fixed, either), Jul (fixed, either, wraps NY),")
|
||||
print(" Vår (flexible, either, 03-01..05-31), Höstlov (fixed,")
|
||||
print(f" specific->{DEMO_NAME}, deliberately underfunded -> shortfall)")
|
||||
print(f" Applied days: {applied_days} planned_leave_day rows for {PARTNER_NAME}")
|
||||
print()
|
||||
print(f"Open: {FRONTEND_URL}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Seed a realistic, ready-to-explore demo workspace."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="Delete the existing demo workspace (only that one) and recreate it.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if settings.database_type != "sqlite":
|
||||
raise SystemExit(
|
||||
f"seed_demo.py only supports sqlite (database_type={settings.database_type!r})"
|
||||
)
|
||||
|
||||
# Defensive: a fresh checkout with no server ever run yet would have no
|
||||
# schema. The dev server does this same call on every boot (see
|
||||
# app/main.py's lifespan) — it's a no-op once already at head.
|
||||
await asyncio.to_thread(run_migrations, f"sqlite:///{settings.sqlite_path}")
|
||||
|
||||
conn = await connect(settings.sqlite_path)
|
||||
try:
|
||||
await _seed(conn, reset=args.reset)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user