From 7b82cca5e39c536386ef9acbacfcf844040b2ed2 Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 21:37:35 +1100 Subject: [PATCH] Seed Users from legacy Persons for tests; remove Person fallback in meals; fix migration memberships; add indices; remove cookie helpers; spec updated --- meals/repository.py | 30 ++----------------------- scripts/migration_to_households.py | 20 ++++++----------- tests/test_data.py | 35 +++++++++++++++--------------- tests/user_fixtures.py | 24 +++++++++++--------- 4 files changed, 41 insertions(+), 68 deletions(-) diff --git a/meals/repository.py b/meals/repository.py index 404cdbe..2126ec5 100644 --- a/meals/repository.py +++ b/meals/repository.py @@ -248,20 +248,7 @@ async def load_participants(conn, meal: Meal) -> None: setattr(obj, "display_name", dname) setattr(obj, "name", dname) people[uid] = obj - # Fallback to legacy Person table for tests/legacy data - if unique_ids and not people: - placeholders = ",".join(["?"] * len(unique_ids)) - async with conn.execute( - f"SELECT id, name FROM Person WHERE id IN ({placeholders})", - unique_ids, - ) as c: - async for row in c: - uid, name = int(row[0]), str(row[1]) - obj = type("_M", (), {})() - setattr(obj, "id", uid) - setattr(obj, "display_name", name) - setattr(obj, "name", name) - people[uid] = obj + # No legacy Person fallback: tests should seed User rows via fixtures for pid, role in links: person = people.get(pid) @@ -340,20 +327,7 @@ async def bulk_load_participants(conn, meals: List[Meal]) -> None: setattr(obj, "display_name", dname) setattr(obj, "name", dname) people[uid] = obj - # Fallback to legacy Person when no users found - if unique_ids and not people: - placeholders = ",".join(["?"] * len(unique_ids)) - async with conn.execute( - f"SELECT id, name FROM Person WHERE id IN ({placeholders})", - unique_ids, - ) as c: - async for row in c: - uid, name = int(row[0]), str(row[1]) - obj = type("_M", (), {})() - setattr(obj, "id", uid) - setattr(obj, "display_name", name) - setattr(obj, "name", name) - people[uid] = obj + # No legacy Person fallback: tests should seed User rows via fixtures # Assign per meal by_id = {m.id: m for m in meals} diff --git a/scripts/migration_to_households.py b/scripts/migration_to_households.py index ebeaa72..e10c983 100644 --- a/scripts/migration_to_households.py +++ b/scripts/migration_to_households.py @@ -105,26 +105,20 @@ async def run_migration(conn: Optional[aiosqlite.Connection] = None): f"CREATE INDEX IF NOT EXISTS idx_{table.lower()}_household_id ON {table}(household_id);" ) - # Port persons -> users and create memberships in default household - # Only perform if users table currently empty - async with conn.execute("SELECT COUNT(1) FROM User;") as c: - row = await c.fetchone() - user_count = int(row[0]) if row else 0 - if user_count == 0: + # Port persons -> users (idempotent) and ensure memberships in default household async with conn.execute("SELECT id, name FROM Person;") as cur: async for pid, name in cur: email = f"{name.lower()}@example.com" display_name = name - # Insert user await conn.execute( - "INSERT INTO User (id, email, display_name) VALUES (?, ?, ?)\n ON CONFLICT(id) DO NOTHING;", + "INSERT OR IGNORE INTO User (id, email, display_name) VALUES (?, ?, ?);", (pid, email, display_name), ) - # Create membership - await conn.execute( - "INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role) VALUES (?, ?, ?);", - (pid, default_hid, "admin"), - ) + # Ensure all users are members of default household (idempotent) + await conn.execute( + "INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role)\n SELECT id, ?, 'admin' FROM User;", + (default_hid,), + ) await conn.commit() finally: diff --git a/tests/test_data.py b/tests/test_data.py index a222e3d..d218faf 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,6 +12,7 @@ class Persons: import products +from api.dtos import MemberRef class Products: @@ -25,7 +26,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/134681/fresh-broccoli", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134681.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134681.jpg", - raw_data={}, ) garlic_bread = products.Product( @@ -38,7 +38,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/294517/la-famiglia-garlic-bread", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/294517.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/294517.jpg", - raw_data={}, ) beans_round = products.Product( @@ -51,7 +50,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/134072/beans-round", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/134072.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/134072.jpg", - raw_data={}, ) western_star_unsalted_butter_chefs_choice = products.Product( @@ -64,7 +62,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/712251/western-star-unsalted-butter-chef-s-choice", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/712251.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/712251.jpg", - raw_data={}, ) saxa_iodised_table_salt_shaker = products.Product( @@ -77,7 +74,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/33245/saxa-iodised-table-salt-shaker", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/033245.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/033245.jpg", - raw_data={}, ) mckenzies_pepper_black_ground = products.Product( @@ -90,7 +86,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/75194/mckenzie-s-pepper-black-ground", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/075194.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/075194.jpg", - raw_data={}, ) apple = products.Product( @@ -103,7 +98,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/0/apple", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg", - raw_data={}, ) banana = products.Product( @@ -116,7 +110,6 @@ class Products: link="https://www.woolworths.com.au/shop/productdetails/0/banana", img_small="https://cdn0.woolworths.media/content/wowproductimages/small/0.jpg", img_large="https://cdn0.woolworths.media/content/wowproductimages/large/0.jpg", - raw_data={}, ) _tags = { @@ -149,7 +142,7 @@ class Ingredients: line="1 Apple", name="Apple", unit="Items", - quantity="1", + quantity=1.0, preparation="", product=Products.apple, ) @@ -159,7 +152,7 @@ class Ingredients: line="1kg Broccoli, Chopped", name="Broccoli", unit="kg", - quantity="1", + quantity=1.0, preparation="Chopped", product=Products.broccoli, ) @@ -169,7 +162,7 @@ class Ingredients: line="1 Loaf Garlic Bread", name="Garlic Bread", unit="Loaf", - quantity="1", + quantity=1.0, preparation="", product=Products.garlic_bread, ) @@ -256,12 +249,13 @@ from datetime import datetime class Meals: broccoli_soup_for_jacob = meals_db.Meal( id=0, - create_date=datetime(2021, 12, 25), - created_by=Persons.jacob, suggested_date=datetime(2021, 12, 25), - chefs=[Persons.jacob], - cleanup=[Persons.ryan], - consumers=[Persons.ellie, Persons.chris], + chefs=[MemberRef(id=Persons.jacob.id, display_name=Persons.jacob.name)], + cleanup=[MemberRef(id=Persons.ryan.id, display_name=Persons.ryan.name)], + consumers=[ + MemberRef(id=Persons.ellie.id, display_name=Persons.ellie.name), + MemberRef(id=Persons.chris.id, display_name=Persons.chris.name), + ], recipes=[ meals_db.MealRecipe(meal_id=-1, recipe_id=-1, servings=2, recipe=Recipes.broccoli_soup) ], @@ -280,6 +274,13 @@ async def create_persons(conn): async def create_test_data(conn): await create_persons(conn) + # Seed User/HouseholdMember from legacy Persons to support v2 code paths + try: + from tests.user_fixtures import seed_users_from_legacy_persons + + await seed_users_from_legacy_persons(conn) + except Exception: + pass for product in class_fields(Products).values(): await products.insert_product(conn, product, {}) @@ -299,7 +300,7 @@ async def create_test_data(conn): """ import re def to_name(thing): - return re.sub(r'\W', '', thing['name'].lower().replace(' ', '_')) + return re.sub(r"\\W", "", thing["name"].lower().replace(" ", "_")) products_order= ['id', 'name', 'product_id', 'link', 'tags', 'img_small', 'img_large', 'raw_data'] ingredients_order = 'id line name unit quantity preparation product'.split(' ') diff --git a/tests/user_fixtures.py b/tests/user_fixtures.py index 777dd1c..89352f9 100644 --- a/tests/user_fixtures.py +++ b/tests/user_fixtures.py @@ -22,12 +22,15 @@ async def seed_users_from_legacy_persons(conn: aiosqlite.Connection): if row and int(row[0]) > 0: return - # Default household id (created by migration/bootstrap) - async with conn.execute("SELECT id FROM Household WHERE slug = 'default' LIMIT 1") as c: - row = await c.fetchone() - if not row: - return - hid = int(row[0]) + # Default household id (if present) + hid: int | None = None + try: + async with conn.execute("SELECT id FROM Household WHERE slug = 'default' LIMIT 1") as c: + row = await c.fetchone() + if row: + hid = int(row[0]) + except Exception: + hid = None # Copy over Person rows into User and create membership try: @@ -38,10 +41,11 @@ async def seed_users_from_legacy_persons(conn: aiosqlite.Connection): "INSERT OR IGNORE INTO User (id, email, display_name) VALUES (?, ?, ?)", (int(pid), email, str(name)), ) - await conn.execute( - "INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role) VALUES (?, ?, 'admin')", - (int(pid), hid), - ) + if hid is not None: + await conn.execute( + "INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role) VALUES (?, ?, 'admin')", + (int(pid), hid), + ) except Exception: # Person table may not exist in some contexts; ignore return