mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-17 13:08:19 -04:00
added bound checking to van invintory
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class ItemCreate(BaseModel):
|
|||||||
"""Schema for creating an item."""
|
"""Schema for creating an item."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
quantity: float = 0
|
quantity: float = Field(default=0, ge=0)
|
||||||
unit: str
|
unit: str
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class ItemUpdate(BaseModel):
|
|||||||
"""Schema for updating an item."""
|
"""Schema for updating an item."""
|
||||||
|
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
quantity: float | None = None
|
quantity: float | None = Field(default=None, ge=0)
|
||||||
unit: str | None = None
|
unit: str | None = None
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class IngredientCreate(BaseModel):
|
|||||||
"""Schema for adding an ingredient to a meal."""
|
"""Schema for adding an ingredient to a meal."""
|
||||||
|
|
||||||
item_id: int
|
item_id: int
|
||||||
quantity_needed: float
|
quantity_needed: float = Field(gt=0)
|
||||||
|
|
||||||
|
|
||||||
class MealCreate(BaseModel):
|
class MealCreate(BaseModel):
|
||||||
@@ -192,6 +192,9 @@ def delete_item(item_id: int, db: DbSession) -> dict[str, bool]:
|
|||||||
@router.post("/meals", response_model=MealResponse)
|
@router.post("/meals", response_model=MealResponse)
|
||||||
def create_meal(meal: MealCreate, db: DbSession) -> MealResponse:
|
def create_meal(meal: MealCreate, db: DbSession) -> MealResponse:
|
||||||
"""Create a new meal with optional ingredients."""
|
"""Create a new meal with optional ingredients."""
|
||||||
|
for ing in meal.ingredients:
|
||||||
|
if not db.get(Item, ing.item_id):
|
||||||
|
raise HTTPException(status_code=422, detail=f"Item {ing.item_id} not found")
|
||||||
db_meal = Meal(name=meal.name, instructions=meal.instructions)
|
db_meal = Meal(name=meal.name, instructions=meal.instructions)
|
||||||
db.add(db_meal)
|
db.add(db_meal)
|
||||||
db.flush()
|
db.flush()
|
||||||
@@ -217,6 +220,15 @@ def list_meals(db: DbSession) -> list[MealResponse]:
|
|||||||
return [MealResponse.from_meal(m) for m in meals]
|
return [MealResponse.from_meal(m) for m in meals]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/meals/availability", response_model=list[MealAvailability])
|
||||||
|
def check_all_meals(db: DbSession) -> list[MealAvailability]:
|
||||||
|
"""Check which meals can be made with current inventory."""
|
||||||
|
meals = list(
|
||||||
|
db.scalars(select(Meal).options(selectinload(Meal.ingredients).selectinload(MealIngredient.item))).all()
|
||||||
|
)
|
||||||
|
return [_check_meal(m) for m in meals]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meals/{meal_id}", response_model=MealResponse)
|
@router.get("/meals/{meal_id}", response_model=MealResponse)
|
||||||
def get_meal(meal_id: int, db: DbSession) -> MealResponse:
|
def get_meal(meal_id: int, db: DbSession) -> MealResponse:
|
||||||
"""Get a meal by ID with ingredients."""
|
"""Get a meal by ID with ingredients."""
|
||||||
@@ -245,6 +257,13 @@ def add_ingredient(meal_id: int, ingredient: IngredientCreate, db: DbSession) ->
|
|||||||
meal = db.get(Meal, meal_id)
|
meal = db.get(Meal, meal_id)
|
||||||
if not meal:
|
if not meal:
|
||||||
raise HTTPException(status_code=404, detail="Meal not found")
|
raise HTTPException(status_code=404, detail="Meal not found")
|
||||||
|
if not db.get(Item, ingredient.item_id):
|
||||||
|
raise HTTPException(status_code=422, detail="Item not found")
|
||||||
|
existing = db.scalar(
|
||||||
|
select(MealIngredient).where(MealIngredient.meal_id == meal_id, MealIngredient.item_id == ingredient.item_id)
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=409, detail="Ingredient already exists for this meal")
|
||||||
db.add(MealIngredient(meal_id=meal_id, item_id=ingredient.item_id, quantity_needed=ingredient.quantity_needed))
|
db.add(MealIngredient(meal_id=meal_id, item_id=ingredient.item_id, quantity_needed=ingredient.quantity_needed))
|
||||||
db.commit()
|
db.commit()
|
||||||
meal = db.scalar(
|
meal = db.scalar(
|
||||||
@@ -264,18 +283,6 @@ def remove_ingredient(meal_id: int, item_id: int, db: DbSession) -> dict[str, bo
|
|||||||
return {"deleted": True}
|
return {"deleted": True}
|
||||||
|
|
||||||
|
|
||||||
# What can I make / what do I need
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meals/availability", response_model=list[MealAvailability])
|
|
||||||
def check_all_meals(db: DbSession) -> list[MealAvailability]:
|
|
||||||
"""Check which meals can be made with current inventory."""
|
|
||||||
meals = list(
|
|
||||||
db.scalars(select(Meal).options(selectinload(Meal.ingredients).selectinload(MealIngredient.item))).all()
|
|
||||||
)
|
|
||||||
return [_check_meal(m) for m in meals]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/meals/{meal_id}/availability", response_model=MealAvailability)
|
@router.get("/meals/{meal_id}/availability", response_model=MealAvailability)
|
||||||
def check_meal(meal_id: int, db: DbSession) -> MealAvailability:
|
def check_meal(meal_id: int, db: DbSession) -> MealAvailability:
|
||||||
"""Check if a specific meal can be made and what's missing."""
|
"""Check if a specific meal can be made and what's missing."""
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -43,6 +43,8 @@ def htmx_create_item(
|
|||||||
category: Annotated[str | None, Form()] = None,
|
category: Annotated[str | None, Form()] = None,
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""Create an item and return updated item rows."""
|
"""Create an item and return updated item rows."""
|
||||||
|
if quantity < 0:
|
||||||
|
raise HTTPException(status_code=422, detail="Quantity must not be negative")
|
||||||
db.add(Item(name=name, quantity=quantity, unit=unit, category=category or None))
|
db.add(Item(name=name, quantity=quantity, unit=unit, category=category or None))
|
||||||
db.commit()
|
db.commit()
|
||||||
items = list(db.scalars(select(Item).order_by(Item.name)).all())
|
items = list(db.scalars(select(Item).order_by(Item.name)).all())
|
||||||
@@ -57,6 +59,8 @@ def htmx_update_item(
|
|||||||
quantity: Annotated[float, Form()],
|
quantity: Annotated[float, Form()],
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""Update an item's quantity and return updated item rows."""
|
"""Update an item's quantity and return updated item rows."""
|
||||||
|
if quantity < 0:
|
||||||
|
raise HTTPException(status_code=422, detail="Quantity must not be negative")
|
||||||
item = db.get(Item, item_id)
|
item = db.get(Item, item_id)
|
||||||
if item:
|
if item:
|
||||||
item.quantity = quantity
|
item.quantity = quantity
|
||||||
@@ -132,6 +136,8 @@ def _load_meal(db: DbSession, meal_id: int) -> Meal | None:
|
|||||||
def meal_detail_page(request: Request, meal_id: int, db: DbSession) -> HTMLResponse:
|
def meal_detail_page(request: Request, meal_id: int, db: DbSession) -> HTMLResponse:
|
||||||
"""Render the meal detail page."""
|
"""Render the meal detail page."""
|
||||||
meal = _load_meal(db, meal_id)
|
meal = _load_meal(db, meal_id)
|
||||||
|
if not meal:
|
||||||
|
raise HTTPException(status_code=404, detail="Meal not found")
|
||||||
items = list(db.scalars(select(Item).order_by(Item.name)).all())
|
items = list(db.scalars(select(Item).order_by(Item.name)).all())
|
||||||
return templates.TemplateResponse(request, "meal_detail.html", {"meal": meal, "items": items})
|
return templates.TemplateResponse(request, "meal_detail.html", {"meal": meal, "items": items})
|
||||||
|
|
||||||
@@ -145,6 +151,18 @@ def htmx_add_ingredient(
|
|||||||
quantity_needed: Annotated[float, Form()],
|
quantity_needed: Annotated[float, Form()],
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""Add an ingredient to a meal and return updated ingredient rows."""
|
"""Add an ingredient to a meal and return updated ingredient rows."""
|
||||||
|
if quantity_needed <= 0:
|
||||||
|
raise HTTPException(status_code=422, detail="Quantity must be positive")
|
||||||
|
meal = db.get(Meal, meal_id)
|
||||||
|
if not meal:
|
||||||
|
raise HTTPException(status_code=404, detail="Meal not found")
|
||||||
|
if not db.get(Item, item_id):
|
||||||
|
raise HTTPException(status_code=422, detail="Item not found")
|
||||||
|
existing = db.scalar(
|
||||||
|
select(MealIngredient).where(MealIngredient.meal_id == meal_id, MealIngredient.item_id == item_id)
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=409, detail="Ingredient already exists for this meal")
|
||||||
db.add(MealIngredient(meal_id=meal_id, item_id=item_id, quantity_needed=quantity_needed))
|
db.add(MealIngredient(meal_id=meal_id, item_id=item_id, quantity_needed=quantity_needed))
|
||||||
db.commit()
|
db.commit()
|
||||||
meal = _load_meal(db, meal_id)
|
meal = _load_meal(db, meal_id)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
--bg-input: #111128;
|
--bg-input: #111128;
|
||||||
--border: #1a1a3e;
|
--border: #1a1a3e;
|
||||||
--text: #c0c0d0;
|
--text: #c0c0d0;
|
||||||
--text-dim: #666680;
|
--text-dim: #8e8ea0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Van Inventory</h1>
|
<h1>Van Inventory</h1>
|
||||||
|
|
||||||
<form hx-post="/items" hx-target="#item-list" hx-swap="innerHTML" hx-on::after-request="this.reset()">
|
<form hx-post="/items" hx-target="#item-list" hx-swap="innerHTML" hx-on::after-request="if(event.detail.successful) this.reset()">
|
||||||
<label>Name <input type="text" name="name" required></label>
|
<label>Name <input type="text" name="name" required></label>
|
||||||
<label>Qty <input type="number" name="quantity" step="any" value="0" required></label>
|
<label>Qty <input type="number" name="quantity" step="any" value="0" min="0" required></label>
|
||||||
<label>Unit <input type="text" name="unit" required placeholder="lbs, cans, etc"></label>
|
<label>Unit <input type="text" name="unit" required placeholder="lbs, cans, etc"></label>
|
||||||
<label>Category <input type="text" name="category" placeholder="optional"></label>
|
<label>Category <input type="text" name="category" placeholder="optional"></label>
|
||||||
<button type="submit">Add Item</button>
|
<button type="submit">Add Item</button>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% if meal.instructions %}<p>{{ meal.instructions }}</p>{% endif %}
|
{% if meal.instructions %}<p>{{ meal.instructions }}</p>{% endif %}
|
||||||
|
|
||||||
<h2>Ingredients</h2>
|
<h2>Ingredients</h2>
|
||||||
<form hx-post="/meals/{{ meal.id }}/ingredients" hx-target="#ingredient-list" hx-swap="innerHTML" hx-on::after-request="this.reset()">
|
<form hx-post="/meals/{{ meal.id }}/ingredients" hx-target="#ingredient-list" hx-swap="innerHTML" hx-on::after-request="if(event.detail.successful) this.reset()">
|
||||||
<label>Item
|
<label>Item
|
||||||
<select name="item_id" required>
|
<select name="item_id" required>
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Qty needed <input type="number" name="quantity_needed" step="any" required></label>
|
<label>Qty needed <input type="number" name="quantity_needed" step="any" min="0.01" required></label>
|
||||||
<button type="submit">Add</button>
|
<button type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Meals</h1>
|
<h1>Meals</h1>
|
||||||
|
|
||||||
<form hx-post="/meals" hx-target="#meal-list" hx-swap="innerHTML" hx-on::after-request="this.reset()">
|
<form hx-post="/meals" hx-target="#meal-list" hx-swap="innerHTML" hx-on::after-request="if(event.detail.successful) this.reset()">
|
||||||
<label>Name <input type="text" name="name" required></label>
|
<label>Name <input type="text" name="name" required></label>
|
||||||
<label>Instructions <input type="text" name="instructions" placeholder="optional"></label>
|
<label>Instructions <input type="text" name="instructions" placeholder="optional"></label>
|
||||||
<button type="submit">Add Meal</button>
|
<button type="submit">Add Meal</button>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form hx-patch="/items/{{ item.id }}" hx-target="#item-list" hx-swap="innerHTML" style="display:inline; margin:0;">
|
<form hx-patch="/items/{{ item.id }}" hx-target="#item-list" hx-swap="innerHTML" style="display:inline; margin:0;">
|
||||||
<input type="number" name="quantity" value="{{ item.quantity }}" step="any" style="width:5rem">
|
<input type="number" name="quantity" value="{{ item.quantity }}" step="any" min="0" style="width:5rem">
|
||||||
<button type="submit" style="padding:0.2rem 0.5rem; font-size:0.8rem;">Update</button>
|
<button type="submit" style="padding:0.2rem 0.5rem; font-size:0.8rem;">Update</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user