143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Dict, List, Literal, cast
|
|
from enum import Enum
|
|
|
|
import ingredients
|
|
import meals
|
|
from api.dtos import MemberRef
|
|
import recipes
|
|
import shopping
|
|
from common import ApiModel, Field
|
|
from shopping.models import StoreEnum
|
|
|
|
|
|
# Outward-facing models and mapping helpers shared by shopping API
|
|
class ListIngredientItem(ApiModel):
|
|
kind: Literal["ingredient"] = "ingredient"
|
|
id: int = -1
|
|
ingredient_id: int
|
|
person_id: int
|
|
created_date: datetime
|
|
# These may be present when the ingredient is part of a requested meal
|
|
list_id: int | None = None
|
|
meal_id: int | None = None
|
|
recipe_id: int | None = None
|
|
|
|
|
|
class RequestedMealItem(ApiModel):
|
|
kind: Literal["requestedMeal"] = "requestedMeal"
|
|
id: int = -1
|
|
person_id: int
|
|
meal_id: int
|
|
created_date: datetime
|
|
|
|
|
|
# Input DTOs (separate from internal DB/domain models)
|
|
class IngredientPurchaseItemIn(ApiModel):
|
|
ingredient_id: int
|
|
person_id: int
|
|
created_date: datetime | None = None
|
|
meal_id: int | None = None
|
|
recipe_id: int | None = None
|
|
|
|
|
|
class PurchaseListIn(ApiModel):
|
|
store_name: StoreEnum
|
|
items: List[IngredientPurchaseItemIn]
|
|
|
|
|
|
# Output DTOs for purchased lists
|
|
class StoreNameOut(str, Enum):
|
|
woolworths = "woolworths"
|
|
coles = "coles"
|
|
home = "home"
|
|
|
|
|
|
class ShoppingListOut(ApiModel):
|
|
id: int
|
|
created_date: datetime
|
|
# outward-only enum values: include "home" instead of an empty string
|
|
store_name: Literal["woolworths", "coles", "home"]
|
|
purchased_by_id: int
|
|
purchased_by: MemberRef | None = None
|
|
# Make items required in the schema; callers must always send an array (possibly empty)
|
|
items: List[ListIngredientItem] = Field(min_length=0, json_schema_extra={"minItems": 0})
|
|
|
|
|
|
class CurrentShoppingList(ApiModel):
|
|
outstanding_items: List[ListIngredientItem] = Field(
|
|
min_length=0, json_schema_extra={"minItems": 0}
|
|
)
|
|
requested_meals: List[RequestedMealItem] = Field(
|
|
min_length=0, json_schema_extra={"minItems": 0}
|
|
)
|
|
# Make all collections required to avoid undefined/null semantics in clients
|
|
purchased_items: List[ListIngredientItem] = Field(
|
|
min_length=0, json_schema_extra={"minItems": 0}
|
|
)
|
|
|
|
ingredients_lookup: Dict[int, ingredients.Ingredient]
|
|
meals_lookup: Dict[int, meals.Meal]
|
|
shopping_list_lookup: Dict[int, ShoppingListOut]
|
|
recipes_lookup: Dict[int, recipes.Recipe]
|
|
|
|
|
|
class PurchasedShoppingList(ApiModel):
|
|
list: ShoppingListOut
|
|
# Lookup maps are required to be present (may be empty)
|
|
meals_lookup: Dict[int, meals.Meal]
|
|
ingredients_lookup: Dict[int, ingredients.Ingredient]
|
|
recipes_lookup: Dict[int, recipes.Recipe]
|
|
|
|
|
|
def _to_ingredient_item(item: shopping.ShoppingListItem) -> ListIngredientItem:
|
|
return ListIngredientItem(
|
|
id=item.id,
|
|
ingredient_id=item.ingredient_id if item.ingredient_id is not None else -1,
|
|
person_id=item.person_id,
|
|
created_date=item.created_date,
|
|
list_id=item.list_id,
|
|
meal_id=item.meal_id,
|
|
recipe_id=item.recipe_id,
|
|
)
|
|
|
|
|
|
def _to_meal_item(item: shopping.ShoppingListItem) -> RequestedMealItem:
|
|
return RequestedMealItem(
|
|
id=item.id,
|
|
person_id=item.person_id,
|
|
meal_id=item.meal_id if item.meal_id is not None else -1,
|
|
created_date=item.created_date,
|
|
)
|
|
|
|
|
|
def _to_shopping_list_out(sl: shopping.ShoppingList) -> ShoppingListOut:
|
|
# Map internal enum value "" to outward-friendly "home"
|
|
if sl.store_name == StoreEnum.home:
|
|
outward_store: Literal["woolworths", "coles", "home"] = "home"
|
|
else:
|
|
# Remaining enum values are 'woolworths' or 'coles'
|
|
outward_store = cast(Literal["woolworths", "coles"], sl.store_name.value)
|
|
# Map purchased_by if present: expect ShoppingList.purchased_by to carry display_name if available
|
|
mref = None
|
|
if sl.purchased_by is not None:
|
|
try:
|
|
# sl.purchased_by may be a lightweight object with id/name
|
|
pid = getattr(sl.purchased_by, "id", None)
|
|
pname = getattr(sl.purchased_by, "name", None) or getattr(
|
|
sl.purchased_by, "display_name", None
|
|
)
|
|
if isinstance(pid, int) and pname:
|
|
mref = MemberRef(id=pid, display_name=pname)
|
|
except Exception:
|
|
mref = None
|
|
return ShoppingListOut(
|
|
id=sl.id,
|
|
created_date=sl.created_date,
|
|
store_name=outward_store,
|
|
purchased_by_id=sl.purchased_by_id,
|
|
purchased_by=mref,
|
|
items=[_to_ingredient_item(i) for i in sl.items],
|
|
)
|