[preset] implement
This commit is contained in:
parent
50b49c8bb7
commit
705baa2857
18 changed files with 533 additions and 7 deletions
30
Makefile
Normal file
30
Makefile
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
VERSION=0.`git rev-list --count HEAD`
|
||||||
|
.PHONY: black
|
||||||
|
|
||||||
|
|
||||||
|
black:
|
||||||
|
black .
|
||||||
|
|
||||||
|
.PHONY: mypy
|
||||||
|
mypy:
|
||||||
|
mypy .
|
||||||
|
|
||||||
|
.PHONY: flake
|
||||||
|
flake:
|
||||||
|
flake8 .
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: black mypy flake
|
||||||
|
|
||||||
|
.PHONY: version
|
||||||
|
version:
|
||||||
|
@echo $(VERSION)
|
||||||
|
|
||||||
|
.PHONY: create-venv
|
||||||
|
create-venv:
|
||||||
|
python3 -m venv .venv --prompt="fooderapi-venv" --system-site-packages
|
||||||
|
bash -c "source .venv/bin/activate && pip install -r requirements_local.txt"
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
./test.sh
|
|
@ -1,9 +1,14 @@
|
||||||
from typing import AsyncIterator
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from ..model.meal import Meal, CreateMealPayload
|
from ..model.meal import (
|
||||||
|
Meal,
|
||||||
|
CreateMealPayload,
|
||||||
|
SaveMealPayload,
|
||||||
|
CreateMealFromPresetPayload,
|
||||||
|
)
|
||||||
from ..domain.meal import Meal as DBMeal
|
from ..domain.meal import Meal as DBMeal
|
||||||
from ..domain.diary import Diary as DBDiary
|
from ..domain.diary import Diary as DBDiary
|
||||||
|
from ..domain.preset import Preset as DBPreset
|
||||||
from .base import AuthorizedController
|
from .base import AuthorizedController
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,3 +27,40 @@ class CreateMeal(AuthorizedController):
|
||||||
return Meal.from_orm(meal)
|
return Meal.from_orm(meal)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
raise HTTPException(status_code=400, detail=e.args[0])
|
raise HTTPException(status_code=400, detail=e.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
class SaveMeal(AuthorizedController):
|
||||||
|
async def call(self, meal_id: id, payload: SaveMealPayload) -> None:
|
||||||
|
async with self.async_session.begin() as session:
|
||||||
|
meal = await DBMeal.get_by_id(session, self.user.id, meal_id)
|
||||||
|
if meal is None:
|
||||||
|
raise HTTPException(status_code=404, detail="meal not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await DBPreset.create(
|
||||||
|
session, user_id=self.user.id, name=payload.name, meal=meal
|
||||||
|
)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=e.args[0])
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMealFromPreset(AuthorizedController):
|
||||||
|
async def call(self, content: CreateMealFromPresetPayload) -> Meal:
|
||||||
|
async with self.async_session.begin() as session:
|
||||||
|
if not await DBDiary.has_permission(
|
||||||
|
session, self.user.id, content.diary_id
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=404, detail="diary not found")
|
||||||
|
|
||||||
|
preset = await DBPreset.get(session, self.user.id, content.preset_id)
|
||||||
|
|
||||||
|
if preset is None:
|
||||||
|
raise HTTPException(status_code=404, detail="preset not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
meal = await DBMeal.create_from_preset(
|
||||||
|
session, content.diary_id, content.order, content.name, preset
|
||||||
|
)
|
||||||
|
return Meal.from_orm(meal)
|
||||||
|
except AssertionError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=e.args[0])
|
||||||
|
|
16
fooder/controller/preset.py
Normal file
16
fooder/controller/preset.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from typing import AsyncIterator, Optional
|
||||||
|
|
||||||
|
from ..model.preset import Preset
|
||||||
|
from ..domain.preset import Preset as DBPreset
|
||||||
|
from .base import AuthorizedController
|
||||||
|
|
||||||
|
|
||||||
|
class ListPresets(AuthorizedController):
|
||||||
|
async def call(
|
||||||
|
self, limit: int, offset: int, q: Optional[str]
|
||||||
|
) -> AsyncIterator[Preset]:
|
||||||
|
async with self.async_session() as session:
|
||||||
|
async for preset in DBPreset.list_all(
|
||||||
|
session, limit=limit, offset=offset, q=q
|
||||||
|
):
|
||||||
|
yield Preset.from_orm(preset)
|
|
@ -5,3 +5,5 @@ from .meal import Meal
|
||||||
from .product import Product
|
from .product import Product
|
||||||
from .user import User
|
from .user import User
|
||||||
from .token import RefreshToken
|
from .token import RefreshToken
|
||||||
|
from .preset import Preset
|
||||||
|
from .preset_entry import PresetEntry
|
||||||
|
|
|
@ -107,14 +107,14 @@ class Entry(Base, CommonMixin):
|
||||||
if meal_id is not None:
|
if meal_id is not None:
|
||||||
self.meal_id = meal_id
|
self.meal_id = meal_id
|
||||||
try:
|
try:
|
||||||
session.flush()
|
await session.flush()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise AssertionError("meal does not exist")
|
raise AssertionError("meal does not exist")
|
||||||
|
|
||||||
if product_id is not None:
|
if product_id is not None:
|
||||||
self.product_id = product_id
|
self.product_id = product_id
|
||||||
try:
|
try:
|
||||||
session.flush()
|
await session.flush()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise AssertionError("product does not exist")
|
raise AssertionError("product does not exist")
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from typing import Optional
|
||||||
|
|
||||||
from .base import Base, CommonMixin
|
from .base import Base, CommonMixin
|
||||||
from .entry import Entry
|
from .entry import Entry
|
||||||
|
from .preset import Preset
|
||||||
|
|
||||||
|
|
||||||
class Meal(Base, CommonMixin):
|
class Meal(Base, CommonMixin):
|
||||||
|
@ -87,6 +88,39 @@ class Meal(Base, CommonMixin):
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
return meal
|
return meal
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_from_preset(
|
||||||
|
cls,
|
||||||
|
session: AsyncSession,
|
||||||
|
diary_id: int,
|
||||||
|
order: int,
|
||||||
|
name: Optional[str],
|
||||||
|
preset: Preset,
|
||||||
|
) -> "Meal":
|
||||||
|
# check if order already exists in diary
|
||||||
|
query = select(cls).where(cls.diary_id == diary_id).where(cls.order == order)
|
||||||
|
existing_meal = await session.scalar(query)
|
||||||
|
assert existing_meal is None, "order already exists in diary"
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
name = preset.name or f"Meal {order}"
|
||||||
|
|
||||||
|
meal = Meal(diary_id=diary_id, name=name, order=order)
|
||||||
|
session.add(meal)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.flush()
|
||||||
|
except IntegrityError:
|
||||||
|
raise AssertionError("diary does not exist")
|
||||||
|
|
||||||
|
for entry in preset.entries:
|
||||||
|
await Entry.create(session, meal.id, entry.product_id, entry.grams)
|
||||||
|
|
||||||
|
meal = await cls._get_by_id(session, meal.id)
|
||||||
|
if not meal:
|
||||||
|
raise RuntimeError()
|
||||||
|
return meal
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_by_id(cls, session: AsyncSession, id: int) -> "Optional[Meal]":
|
async def _get_by_id(cls, session: AsyncSession, id: int) -> "Optional[Meal]":
|
||||||
"""get_by_id."""
|
"""get_by_id."""
|
||||||
|
|
104
fooder/domain/preset.py
Normal file
104
fooder/domain/preset.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
from sqlalchemy.orm import relationship, Mapped, mapped_column, joinedload
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import ForeignKey, Integer, select
|
||||||
|
|
||||||
|
from .base import Base, CommonMixin
|
||||||
|
from .preset_entry import PresetEntry
|
||||||
|
from typing import AsyncIterator, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .meal import Meal
|
||||||
|
|
||||||
|
|
||||||
|
class Preset(Base, CommonMixin):
|
||||||
|
"""Preset."""
|
||||||
|
|
||||||
|
name: Mapped[str]
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"))
|
||||||
|
entries: Mapped[list[PresetEntry]] = relationship(
|
||||||
|
lazy="selectin", order_by=PresetEntry.last_changed
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def calories(self) -> float:
|
||||||
|
"""calories.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return sum(entry.calories for entry in self.entries)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protein(self) -> float:
|
||||||
|
"""protein.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return sum(entry.protein for entry in self.entries)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def carb(self) -> float:
|
||||||
|
"""carb.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return sum(entry.carb for entry in self.entries)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fat(self) -> float:
|
||||||
|
"""fat.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return sum(entry.fat for entry in self.entries)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fiber(self) -> float:
|
||||||
|
"""fiber.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return sum(entry.fiber for entry in self.entries)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, session: AsyncSession, user_id: int, name: str, meal: "Meal"
|
||||||
|
) -> None:
|
||||||
|
preset = Preset(user_id=user_id, name=name)
|
||||||
|
|
||||||
|
session.add(preset)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.flush()
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError()
|
||||||
|
|
||||||
|
for entry in meal.entries:
|
||||||
|
PresetEntry.create(session, preset.id, entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def list_all(
|
||||||
|
cls, session: AsyncSession, offset: int, limit: int, q: Optional[str] = None
|
||||||
|
) -> AsyncIterator["Preset"]:
|
||||||
|
query = select(cls)
|
||||||
|
|
||||||
|
if q:
|
||||||
|
query = query.filter(cls.name.ilike(f"%{q.lower()}%"))
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
stream = await session.stream_scalars(query.order_by(cls.id))
|
||||||
|
async for row in stream:
|
||||||
|
yield row
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get(
|
||||||
|
cls, session: AsyncSession, user_id: int, preset_id: int
|
||||||
|
) -> "Optional[Preset]":
|
||||||
|
"""get."""
|
||||||
|
query = (
|
||||||
|
select(cls)
|
||||||
|
.where(cls.id == preset_id)
|
||||||
|
.where(cls.user_id == user_id)
|
||||||
|
.options(joinedload(cls.entries).joinedload(PresetEntry.product))
|
||||||
|
)
|
||||||
|
return await session.scalar(query)
|
88
fooder/domain/preset_entry.py
Normal file
88
fooder/domain/preset_entry.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy import ForeignKey, Integer, DateTime
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .base import Base, CommonMixin
|
||||||
|
from .product import Product
|
||||||
|
from .entry import Entry
|
||||||
|
|
||||||
|
|
||||||
|
class PresetEntry(Base, CommonMixin):
|
||||||
|
"""Entry."""
|
||||||
|
|
||||||
|
grams: Mapped[float]
|
||||||
|
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
||||||
|
product: Mapped[Product] = relationship(lazy="selectin")
|
||||||
|
preset_id: Mapped[int] = mapped_column(Integer, ForeignKey("preset.id"))
|
||||||
|
last_changed: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def amount(self) -> float:
|
||||||
|
"""amount.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.grams / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def calories(self) -> float:
|
||||||
|
"""calories.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.amount * self.product.calories
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protein(self) -> float:
|
||||||
|
"""protein.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.amount * self.product.protein
|
||||||
|
|
||||||
|
@property
|
||||||
|
def carb(self) -> float:
|
||||||
|
"""carb.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.amount * self.product.carb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fat(self) -> float:
|
||||||
|
"""fat.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.amount * self.product.fat
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fiber(self) -> float:
|
||||||
|
"""fiber.
|
||||||
|
|
||||||
|
:rtype: float
|
||||||
|
"""
|
||||||
|
return self.amount * self.product.fiber
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
preset_id: int,
|
||||||
|
entry: Entry,
|
||||||
|
) -> None:
|
||||||
|
pentry = PresetEntry(
|
||||||
|
preset_id=preset_id,
|
||||||
|
product_id=entry.product_id,
|
||||||
|
grams=entry.grams,
|
||||||
|
)
|
||||||
|
session.add(pentry)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.flush()
|
||||||
|
except IntegrityError:
|
||||||
|
raise AssertionError("preset or product does not exist")
|
|
@ -4,7 +4,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from typing import AsyncIterator, Optional
|
from typing import AsyncIterator, Optional
|
||||||
|
|
||||||
from .base import Base, CommonMixin
|
from .base import Base, CommonMixin
|
||||||
from .user import User
|
|
||||||
|
|
||||||
|
|
||||||
class Product(Base, CommonMixin):
|
class Product(Base, CommonMixin):
|
||||||
|
|
|
@ -27,3 +27,18 @@ class CreateMealPayload(BaseModel):
|
||||||
name: Optional[str]
|
name: Optional[str]
|
||||||
order: int
|
order: int
|
||||||
diary_id: int
|
diary_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class SaveMealPayload(BaseModel):
|
||||||
|
"""SaveMealPayload."""
|
||||||
|
|
||||||
|
name: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateMealFromPresetPayload(BaseModel):
|
||||||
|
"""CreateMealPayload."""
|
||||||
|
|
||||||
|
name: Optional[str]
|
||||||
|
order: int
|
||||||
|
diary_id: int
|
||||||
|
preset_id: int
|
||||||
|
|
30
fooder/model/preset.py
Normal file
30
fooder/model/preset.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List
|
||||||
|
from .preset_entry import PresetEntry
|
||||||
|
|
||||||
|
|
||||||
|
class Preset(BaseModel):
|
||||||
|
"""Preset."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
calories: float
|
||||||
|
protein: float
|
||||||
|
carb: float
|
||||||
|
fat: float
|
||||||
|
fiber: float
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PresetDetails(Preset):
|
||||||
|
"""PresetDetails."""
|
||||||
|
|
||||||
|
entries: List[PresetEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class ListPresetsPayload(BaseModel):
|
||||||
|
"""ListPresetsPayload."""
|
||||||
|
|
||||||
|
presets: List[Preset]
|
21
fooder/model/preset_entry.py
Normal file
21
fooder/model/preset_entry.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .product import Product
|
||||||
|
|
||||||
|
|
||||||
|
class PresetEntry(BaseModel):
|
||||||
|
"""PresetEntry."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
grams: float
|
||||||
|
product: Product
|
||||||
|
preset_id: int
|
||||||
|
calories: float
|
||||||
|
protein: float
|
||||||
|
carb: float
|
||||||
|
fat: float
|
||||||
|
fiber: float
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
|
@ -5,6 +5,7 @@ from .view.meal import router as meal_router
|
||||||
from .view.entry import router as entry_router
|
from .view.entry import router as entry_router
|
||||||
from .view.token import router as token_router
|
from .view.token import router as token_router
|
||||||
from .view.user import router as user_router
|
from .view.user import router as user_router
|
||||||
|
from .view.preset import router as preset_router
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
@ -14,3 +15,4 @@ router.include_router(meal_router, prefix="/meal", tags=["meal"])
|
||||||
router.include_router(entry_router, prefix="/entry", tags=["entry"])
|
router.include_router(entry_router, prefix="/entry", tags=["entry"])
|
||||||
router.include_router(token_router, prefix="/token", tags=["token"])
|
router.include_router(token_router, prefix="/token", tags=["token"])
|
||||||
router.include_router(user_router, prefix="/user", tags=["user"])
|
router.include_router(user_router, prefix="/user", tags=["user"])
|
||||||
|
router.include_router(preset_router, prefix="/preset", tags=["preset"])
|
||||||
|
|
23
fooder/test/fixtures/meal.py
vendored
23
fooder/test/fixtures/meal.py
vendored
|
@ -12,3 +12,26 @@ def meal_payload_factory() -> Callable[[int, int], dict[str, int | str]]:
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def meal_save_payload() -> Callable[[int], dict[str, str]]:
|
||||||
|
def factory(meal_id: int) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"name": "new name",
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def meal_from_preset() -> Callable[[int, int, int], dict[str, str | int]]:
|
||||||
|
def factory(order: int, diary_id: int, preset_id: int) -> dict[str, str | int]:
|
||||||
|
return {
|
||||||
|
"name": "new name",
|
||||||
|
"order": order,
|
||||||
|
"diary_id": diary_id,
|
||||||
|
"preset_id": preset_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
69
fooder/test/test_preset.py
Normal file
69
fooder/test/test_preset.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import datetime
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.dependency()
|
||||||
|
def test_create_meal(client, meal_payload_factory):
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
response = client.get("diary", params={"date": today})
|
||||||
|
|
||||||
|
diary_id = response.json()["id"]
|
||||||
|
meal_order = len(response.json()["meals"]) + 1
|
||||||
|
|
||||||
|
response = client.post("meal", json=meal_payload_factory(diary_id, meal_order))
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.dependency(depends=["test_create_meal"])
|
||||||
|
def test_save_meal(client, meal_save_payload):
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
response = client.get("diary", params={"date": today})
|
||||||
|
|
||||||
|
meal_id = response.json()["meals"][0]["id"]
|
||||||
|
save_payload = meal_save_payload(meal_id)
|
||||||
|
|
||||||
|
response = client.post(f"meal/{meal_id}/save", json=save_payload)
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.dependency(depends=["test_create_meal"])
|
||||||
|
def test_list_presets(client, meal_save_payload):
|
||||||
|
response = client.get("preset")
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
assert len(response.json()["presets"]) > 0, response.json()
|
||||||
|
|
||||||
|
name = meal_save_payload(0)["name"]
|
||||||
|
response = client.get(f"preset?q={name}")
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
assert len(response.json()["presets"]) > 0, response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.dependency(depends=["test_list_presets"])
|
||||||
|
def test_create_meal_from_preset(client, meal_from_preset):
|
||||||
|
today = datetime.date.today().isoformat()
|
||||||
|
response = client.get("diary", params={"date": today})
|
||||||
|
|
||||||
|
diary_id = response.json()["id"]
|
||||||
|
meal_order = len(response.json()["meals"]) + 1
|
||||||
|
|
||||||
|
response = client.get("preset")
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
assert len(response.json()["presets"]) > 0, response.json()
|
||||||
|
|
||||||
|
preset = response.json()["presets"][0]
|
||||||
|
|
||||||
|
payload = meal_from_preset(
|
||||||
|
meal_order,
|
||||||
|
diary_id,
|
||||||
|
preset["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post("meal/from_preset", json=payload)
|
||||||
|
assert response.status_code == 200, response.json()
|
||||||
|
meal = response.json()
|
||||||
|
|
||||||
|
for k, v in preset.items():
|
||||||
|
if k in ("id", "name", "entries"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert meal[k] == v, f"{k} != {v}"
|
|
@ -1,6 +1,11 @@
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from ..model.meal import Meal, CreateMealPayload
|
from ..model.meal import (
|
||||||
from ..controller.meal import CreateMeal
|
Meal,
|
||||||
|
CreateMealPayload,
|
||||||
|
SaveMealPayload,
|
||||||
|
CreateMealFromPresetPayload,
|
||||||
|
)
|
||||||
|
from ..controller.meal import CreateMeal, SaveMeal, CreateMealFromPreset
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=["meal"])
|
router = APIRouter(tags=["meal"])
|
||||||
|
@ -13,3 +18,22 @@ async def create_meal(
|
||||||
contoller: CreateMeal = Depends(CreateMeal),
|
contoller: CreateMeal = Depends(CreateMeal),
|
||||||
):
|
):
|
||||||
return await contoller.call(data)
|
return await contoller.call(data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{meal_id}/save")
|
||||||
|
async def save_meal(
|
||||||
|
request: Request,
|
||||||
|
meal_id: int,
|
||||||
|
data: SaveMealPayload,
|
||||||
|
contoller: SaveMeal = Depends(SaveMeal),
|
||||||
|
):
|
||||||
|
await contoller.call(meal_id, data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/from_preset", response_model=Meal)
|
||||||
|
async def create_meal_from_preset(
|
||||||
|
request: Request,
|
||||||
|
data: CreateMealFromPresetPayload,
|
||||||
|
contoller: CreateMealFromPreset = Depends(CreateMealFromPreset),
|
||||||
|
):
|
||||||
|
return await contoller.call(data)
|
||||||
|
|
19
fooder/view/preset.py
Normal file
19
fooder/view/preset.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from ..model.preset import ListPresetsPayload
|
||||||
|
from ..controller.preset import ListPresets
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(tags=["preset"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=ListPresetsPayload)
|
||||||
|
async def list_presets(
|
||||||
|
request: Request,
|
||||||
|
limit: int = 10,
|
||||||
|
offset: int = 0,
|
||||||
|
q: str | None = None,
|
||||||
|
controller: ListPresets = Depends(ListPresets),
|
||||||
|
):
|
||||||
|
return ListPresetsPayload(
|
||||||
|
presets=[p async for p in controller.call(limit=limit, offset=offset, q=q)]
|
||||||
|
)
|
8
requirements_local.txt
Normal file
8
requirements_local.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
fastapi
|
||||||
|
pydantic
|
||||||
|
pydantic_settings
|
||||||
|
sqlalchemy[postgresql_asyncpg]
|
||||||
|
uvicorn[standard]
|
||||||
|
python-jose[cryptography]
|
||||||
|
passlib[bcrypt]
|
||||||
|
fastapi-users
|
Loading…
Reference in a new issue