[diary] most of the logic implemented + changed imports to absolute
This commit is contained in:
parent
45c0a91e1e
commit
f2dd9bfea4
35 changed files with 822 additions and 31 deletions
|
|
@ -11,8 +11,8 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from .domain import Base
|
from fooder.domain import Base
|
||||||
from .settings import settings
|
from fooder.settings import settings
|
||||||
|
|
||||||
engine = sqlalchemy.create_engine(
|
engine = sqlalchemy.create_engine(
|
||||||
settings.DB_URI.replace("+asyncpg", "").replace("+aiosqlite", "")
|
settings.DB_URI.replace("+asyncpg", "").replace("+aiosqlite", "")
|
||||||
|
|
|
||||||
19
fooder/command/create_diary.py
Normal file
19
fooder/command/create_diary.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fooder.context import Context
|
||||||
|
from fooder.controller.diary import DiaryController
|
||||||
|
from fooder.controller.meal import MealController
|
||||||
|
from fooder.domain import Diary
|
||||||
|
from fooder.model.meal import MealCreateModel
|
||||||
|
|
||||||
|
|
||||||
|
async def create_diary(ctx: Context, date: datetime.date) -> Diary:
|
||||||
|
settings = await ctx.repo.user_settings.get_by_user_id(ctx.user.id)
|
||||||
|
diary_ctrl = await DiaryController.create(ctx, date=date, settings=settings)
|
||||||
|
await MealController.create(
|
||||||
|
ctx,
|
||||||
|
diary_id=diary_ctrl.obj.id,
|
||||||
|
data=MealCreateModel(name="Breakfast"),
|
||||||
|
)
|
||||||
|
await ctx.repo.diary.session.refresh(diary_ctrl.obj)
|
||||||
|
return diary_ctrl.obj
|
||||||
13
fooder/command/create_entry.py
Normal file
13
fooder/command/create_entry.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from fooder.context import Context
|
||||||
|
from fooder.controller.entry import EntryController
|
||||||
|
from fooder.domain import Entry
|
||||||
|
from fooder.model.entry import EntryCreateModel
|
||||||
|
|
||||||
|
|
||||||
|
async def create_entry(ctx: Context, meal_id: int, data: EntryCreateModel) -> Entry:
|
||||||
|
ctrl = await EntryController.create(ctx, meal_id=meal_id, data=data)
|
||||||
|
await ctx.repo.user_product_usage.increment(
|
||||||
|
user_id=ctx.user.id,
|
||||||
|
product_id=data.product_id,
|
||||||
|
)
|
||||||
|
return ctrl.obj
|
||||||
38
fooder/controller/diary.py
Normal file
38
fooder/controller/diary.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fooder.context import Context
|
||||||
|
from fooder.controller.base import ModelController
|
||||||
|
from fooder.domain import Diary
|
||||||
|
from fooder.domain.user_settings import UserSettings
|
||||||
|
from fooder.model.diary import DiaryUpdateModel
|
||||||
|
|
||||||
|
|
||||||
|
class DiaryController(ModelController[Diary]):
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, ctx: Context, date: datetime.date, settings: UserSettings
|
||||||
|
) -> "DiaryController":
|
||||||
|
obj = Diary(
|
||||||
|
user_id=ctx.user.id,
|
||||||
|
date=date,
|
||||||
|
protein_goal=settings.protein_goal,
|
||||||
|
carb_goal=settings.carb_goal,
|
||||||
|
fat_goal=settings.fat_goal,
|
||||||
|
fiber_goal=settings.fiber_goal,
|
||||||
|
calories_goal=settings.calories_goal,
|
||||||
|
)
|
||||||
|
await ctx.repo.diary.create(obj)
|
||||||
|
return cls(ctx, obj)
|
||||||
|
|
||||||
|
async def update(self, data: DiaryUpdateModel) -> None:
|
||||||
|
if data.protein_goal is not None:
|
||||||
|
self.obj.protein_goal = data.protein_goal
|
||||||
|
if data.carb_goal is not None:
|
||||||
|
self.obj.carb_goal = data.carb_goal
|
||||||
|
if data.fat_goal is not None:
|
||||||
|
self.obj.fat_goal = data.fat_goal
|
||||||
|
if data.fiber_goal is not None:
|
||||||
|
self.obj.fiber_goal = data.fiber_goal
|
||||||
|
if data.calories_goal is not None:
|
||||||
|
self.obj.calories_goal = data.calories_goal
|
||||||
|
await self.ctx.repo.diary.update(self.obj)
|
||||||
19
fooder/controller/entry.py
Normal file
19
fooder/controller/entry.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from fooder.context import Context
|
||||||
|
from fooder.controller.base import ModelController
|
||||||
|
from fooder.domain import Entry
|
||||||
|
from fooder.model.entry import EntryCreateModel, EntryUpdateModel
|
||||||
|
|
||||||
|
|
||||||
|
class EntryController(ModelController[Entry]):
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, ctx: Context, meal_id: int, data: EntryCreateModel
|
||||||
|
) -> "EntryController":
|
||||||
|
obj = Entry(grams=data.grams, product_id=data.product_id, meal_id=meal_id)
|
||||||
|
await ctx.repo.entry.create(obj)
|
||||||
|
return cls(ctx, obj)
|
||||||
|
|
||||||
|
async def update(self, data: EntryUpdateModel) -> None:
|
||||||
|
if data.grams is not None:
|
||||||
|
self.obj.grams = data.grams
|
||||||
|
await self.ctx.repo.entry.update(self.obj)
|
||||||
26
fooder/controller/meal.py
Normal file
26
fooder/controller/meal.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from fooder.context import Context
|
||||||
|
from fooder.controller.base import ModelController
|
||||||
|
from fooder.domain import Meal
|
||||||
|
from fooder.model.meal import MealCreateModel, MealUpdateModel
|
||||||
|
|
||||||
|
|
||||||
|
class MealController(ModelController[Meal]):
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, ctx: Context, diary_id: int, data: MealCreateModel
|
||||||
|
) -> "MealController":
|
||||||
|
order = (
|
||||||
|
data.order
|
||||||
|
if data.order is not None
|
||||||
|
else await ctx.repo.meal.next_order(diary_id)
|
||||||
|
)
|
||||||
|
obj = Meal(name=data.name, order=order, diary_id=diary_id)
|
||||||
|
await ctx.repo.meal.create(obj)
|
||||||
|
return cls(ctx, obj)
|
||||||
|
|
||||||
|
async def update(self, data: MealUpdateModel) -> None:
|
||||||
|
if data.name is not None:
|
||||||
|
self.obj.name = data.name
|
||||||
|
if data.order is not None:
|
||||||
|
self.obj.order = data.order
|
||||||
|
await self.ctx.repo.meal.update(self.obj)
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from .base import Base # noqa
|
from fooder.domain.base import Base # noqa
|
||||||
from .diary import Diary # noqa
|
from fooder.domain.diary import Diary # noqa
|
||||||
from .entry import Entry # noqa
|
from fooder.domain.entry import Entry # noqa
|
||||||
from .meal import Meal # noqa
|
from fooder.domain.meal import Meal # noqa
|
||||||
from .product import Product # noqa
|
from fooder.domain.product import Product # noqa
|
||||||
from .user import User # noqa
|
from fooder.domain.user import User # noqa
|
||||||
from .user_product_usage import UserProductUsage # noqa
|
from fooder.domain.user_product_usage import UserProductUsage # noqa
|
||||||
from .user_settings import UserSettings # noqa
|
from fooder.domain.user_settings import UserSettings # noqa
|
||||||
from .preset import Preset # noqa
|
from fooder.domain.preset import Preset # noqa
|
||||||
from .preset_entry import PresetEntry # noqa
|
from fooder.domain.preset_entry import PresetEntry # noqa
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class Diary(Base, CommonMixin):
|
||||||
__table_args__ = (UniqueConstraint("user_id", "date"),)
|
__table_args__ = (UniqueConstraint("user_id", "date"),)
|
||||||
|
|
||||||
meals: Mapped[list[Meal]] = relationship(
|
meals: Mapped[list[Meal]] = relationship(
|
||||||
lazy="selectin", order_by=Meal.order.desc()
|
lazy="selectin", order_by=Meal.order.desc(), cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
date: Mapped[datetime.date] = mapped_column(Date)
|
date: Mapped[datetime.date] = mapped_column(Date)
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"))
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from sqlalchemy import Boolean, ForeignKey, Integer
|
from sqlalchemy import ForeignKey, Integer
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from fooder.domain.base import Base, CommonMixin, EntryMacrosMixin
|
from fooder.domain.base import Base, CommonMixin, EntryMacrosMixin
|
||||||
|
|
@ -12,4 +12,3 @@ class Entry(Base, CommonMixin, EntryMacrosMixin):
|
||||||
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
||||||
product: Mapped[Product] = relationship(lazy="selectin")
|
product: Mapped[Product] = relationship(lazy="selectin")
|
||||||
meal_id: Mapped[int] = mapped_column(Integer, ForeignKey("meal.id"))
|
meal_id: Mapped[int] = mapped_column(Integer, ForeignKey("meal.id"))
|
||||||
processed: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ class Meal(Base, CommonMixin, AggregateMacrosMixin):
|
||||||
order: Mapped[int]
|
order: Mapped[int]
|
||||||
diary_id: Mapped[int] = mapped_column(Integer, ForeignKey("diary.id"))
|
diary_id: Mapped[int] = mapped_column(Integer, ForeignKey("diary.id"))
|
||||||
entries: Mapped[list[Entry]] = relationship(
|
entries: Mapped[list[Entry]] = relationship(
|
||||||
lazy="selectin", order_by=Entry.last_changed
|
lazy="selectin", order_by=Entry.last_changed, cascade="all, delete-orphan"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
33
fooder/model/diary.py
Normal file
33
fooder/model/diary.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from fooder.model.base import ObjModelMixin, Calories
|
||||||
|
from fooder.model.meal import MealModel
|
||||||
|
|
||||||
|
|
||||||
|
class DiaryModel(ObjModelMixin, BaseModel):
|
||||||
|
date: datetime.date
|
||||||
|
protein_goal: Calories
|
||||||
|
carb_goal: Calories
|
||||||
|
fat_goal: Calories
|
||||||
|
fiber_goal: Calories
|
||||||
|
calories_goal: Calories
|
||||||
|
protein: float
|
||||||
|
carb: float
|
||||||
|
fat: float
|
||||||
|
fiber: float
|
||||||
|
calories: float
|
||||||
|
meals: list[MealModel]
|
||||||
|
|
||||||
|
|
||||||
|
class DiaryCreateModel(BaseModel):
|
||||||
|
date: datetime.date
|
||||||
|
|
||||||
|
|
||||||
|
class DiaryUpdateModel(BaseModel):
|
||||||
|
protein_goal: Calories | None = None
|
||||||
|
carb_goal: Calories | None = None
|
||||||
|
fat_goal: Calories | None = None
|
||||||
|
fiber_goal: Calories | None = None
|
||||||
|
calories_goal: Calories | None = None
|
||||||
29
fooder/model/entry.py
Normal file
29
fooder/model/entry.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from fooder.model.base import ObjModelMixin
|
||||||
|
from fooder.model.product import ProductModel
|
||||||
|
|
||||||
|
Grams = Annotated[float, Field(gt=0)]
|
||||||
|
|
||||||
|
|
||||||
|
class EntryModel(ObjModelMixin, BaseModel):
|
||||||
|
grams: Grams
|
||||||
|
product_id: int
|
||||||
|
meal_id: int
|
||||||
|
product: ProductModel
|
||||||
|
protein: float
|
||||||
|
carb: float
|
||||||
|
fat: float
|
||||||
|
fiber: float
|
||||||
|
calories: float
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCreateModel(BaseModel):
|
||||||
|
grams: Grams
|
||||||
|
product_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class EntryUpdateModel(BaseModel):
|
||||||
|
grams: Grams | None = None
|
||||||
30
fooder/model/meal.py
Normal file
30
fooder/model/meal.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from fooder.model.base import ObjModelMixin
|
||||||
|
from fooder.model.entry import EntryModel
|
||||||
|
|
||||||
|
MealOrder = Annotated[int, Field(ge=0)]
|
||||||
|
|
||||||
|
|
||||||
|
class MealModel(ObjModelMixin, BaseModel):
|
||||||
|
name: str
|
||||||
|
order: MealOrder
|
||||||
|
diary_id: int
|
||||||
|
protein: float
|
||||||
|
carb: float
|
||||||
|
fat: float
|
||||||
|
fiber: float
|
||||||
|
calories: float
|
||||||
|
entries: list[EntryModel]
|
||||||
|
|
||||||
|
|
||||||
|
class MealCreateModel(BaseModel):
|
||||||
|
name: str
|
||||||
|
order: MealOrder | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class MealUpdateModel(BaseModel):
|
||||||
|
name: str | None = None
|
||||||
|
order: MealOrder | None = None
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from .base import (
|
from fooder.model.base import (
|
||||||
ObjModelMixin,
|
ObjModelMixin,
|
||||||
Calories,
|
Calories,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from .base import ObjModelMixin, Macronutrient, Calories
|
from fooder.model.base import ObjModelMixin, Macronutrient, Calories
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
15
fooder/repository/diary.py
Normal file
15
fooder/repository/diary.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import datetime
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from fooder.domain import Diary
|
||||||
|
from fooder.repository.base import RepositoryBase, DEFAULT_LIMIT
|
||||||
|
|
||||||
|
|
||||||
|
class DiaryRepository(RepositoryBase[Diary]):
|
||||||
|
async def get_by_user_and_date(self, user_id: int, date: datetime.date) -> Diary:
|
||||||
|
return await self._get(Diary.user_id == user_id, Diary.date == date)
|
||||||
|
|
||||||
|
async def list_by_user(
|
||||||
|
self, user_id: int, offset: int = 0, limit: int | None = DEFAULT_LIMIT
|
||||||
|
) -> Sequence[Diary]:
|
||||||
|
return await self._list(Diary.user_id == user_id, offset=offset, limit=limit)
|
||||||
10
fooder/repository/entry.py
Normal file
10
fooder/repository/entry.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from fooder.domain import Entry
|
||||||
|
from fooder.repository.base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
|
class EntryRepository(RepositoryBase[Entry]):
|
||||||
|
async def get_by_id_and_meal(self, entry_id: int, meal_id: int) -> Entry:
|
||||||
|
return await self._get(Entry.id == entry_id, Entry.meal_id == meal_id)
|
||||||
|
|
||||||
|
async def delete_by_id_and_meal(self, entry_id: int, meal_id: int) -> None:
|
||||||
|
await self._delete(Entry.id == entry_id, Entry.meal_id == meal_id)
|
||||||
18
fooder/repository/meal.py
Normal file
18
fooder/repository/meal.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
from fooder.domain import Meal
|
||||||
|
from fooder.repository.base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
|
class MealRepository(RepositoryBase[Meal]):
|
||||||
|
async def get_by_id_and_diary(self, meal_id: int, diary_id: int) -> Meal:
|
||||||
|
return await self._get(Meal.id == meal_id, Meal.diary_id == diary_id)
|
||||||
|
|
||||||
|
async def delete_by_id_and_diary(self, meal_id: int, diary_id: int) -> None:
|
||||||
|
await self._delete(Meal.id == meal_id, Meal.diary_id == diary_id)
|
||||||
|
|
||||||
|
async def next_order(self, diary_id: int) -> int:
|
||||||
|
stmt = select(func.max(Meal.order)).where(Meal.diary_id == diary_id)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
max_order = result.scalar_one_or_none()
|
||||||
|
return 0 if max_order is None else max_order + 1
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Sequence
|
||||||
|
|
||||||
from fooder.domain import Product
|
from fooder.domain import Product
|
||||||
from fooder.repository.expression import fuzzy_match
|
from fooder.repository.expression import fuzzy_match
|
||||||
from .base import RepositoryBase, DEFAULT_LIMIT
|
from fooder.repository.base import RepositoryBase, DEFAULT_LIMIT
|
||||||
|
|
||||||
|
|
||||||
class ProductRepository(RepositoryBase[Product]):
|
class ProductRepository(RepositoryBase[Product]):
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,18 @@ from fooder.repository.user import UserRepository
|
||||||
from fooder.repository.product import ProductRepository
|
from fooder.repository.product import ProductRepository
|
||||||
from fooder.repository.user_product_usage import UserProductUsageRepository
|
from fooder.repository.user_product_usage import UserProductUsageRepository
|
||||||
from fooder.repository.user_settings import UserSettingsRepository
|
from fooder.repository.user_settings import UserSettingsRepository
|
||||||
from fooder.domain import User, Product, UserProductUsage, UserSettings
|
from fooder.repository.diary import DiaryRepository
|
||||||
|
from fooder.repository.meal import MealRepository
|
||||||
|
from fooder.repository.entry import EntryRepository
|
||||||
|
from fooder.domain import (
|
||||||
|
User,
|
||||||
|
Product,
|
||||||
|
UserProductUsage,
|
||||||
|
UserSettings,
|
||||||
|
Diary,
|
||||||
|
Meal,
|
||||||
|
Entry,
|
||||||
|
)
|
||||||
from fooder.exc import Conflict
|
from fooder.exc import Conflict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -18,6 +29,9 @@ class Repository:
|
||||||
self.product = ProductRepository(Product, session)
|
self.product = ProductRepository(Product, session)
|
||||||
self.user_product_usage = UserProductUsageRepository(UserProductUsage, session)
|
self.user_product_usage = UserProductUsageRepository(UserProductUsage, session)
|
||||||
self.user_settings = UserSettingsRepository(UserSettings, session)
|
self.user_settings = UserSettingsRepository(UserSettings, session)
|
||||||
|
self.diary = DiaryRepository(Diary, session)
|
||||||
|
self.meal = MealRepository(Meal, session)
|
||||||
|
self.entry = EntryRepository(Entry, session)
|
||||||
|
|
||||||
async def commit(self) -> None:
|
async def commit(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fooder.domain import User
|
from fooder.domain import User
|
||||||
from .base import RepositoryBase
|
from fooder.repository.base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
class UserRepository(RepositoryBase[User]):
|
class UserRepository(RepositoryBase[User]):
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ from fooder.repository.base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
class UserProductUsageRepository(RepositoryBase[UserProductUsage]):
|
class UserProductUsageRepository(RepositoryBase[UserProductUsage]):
|
||||||
|
async def get_by_user_and_product(
|
||||||
|
self, user_id: int, product_id: int
|
||||||
|
) -> UserProductUsage:
|
||||||
|
return await self._get(
|
||||||
|
UserProductUsage.user_id == user_id,
|
||||||
|
UserProductUsage.product_id == product_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def increment(self, user_id: int, product_id: int, count: int = 1) -> None:
|
async def increment(self, user_id: int, product_id: int, count: int = 1) -> None:
|
||||||
stmt = (
|
stmt = (
|
||||||
sa_update(UserProductUsage)
|
sa_update(UserProductUsage)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fooder.domain import UserSettings
|
from fooder.domain import UserSettings
|
||||||
from .base import RepositoryBase
|
from fooder.repository.base import RepositoryBase
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsRepository(RepositoryBase[UserSettings]):
|
class UserSettingsRepository(RepositoryBase[UserSettings]):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ from fastapi import APIRouter
|
||||||
from fooder.view.token import router as token_router
|
from fooder.view.token import router as token_router
|
||||||
from fooder.view.product import router as product_router
|
from fooder.view.product import router as product_router
|
||||||
from fooder.view.user_settings import router as user_settings_router
|
from fooder.view.user_settings import router as user_settings_router
|
||||||
|
from fooder.view.diary import router as diary_router
|
||||||
|
from fooder.view.meal import router as meal_router
|
||||||
|
from fooder.view.entry import router as entry_router
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
router.include_router(token_router, prefix="/token", tags=["token"])
|
router.include_router(token_router, prefix="/token", tags=["token"])
|
||||||
|
|
@ -10,3 +13,8 @@ router.include_router(product_router, prefix="/product", tags=["product"])
|
||||||
router.include_router(
|
router.include_router(
|
||||||
user_settings_router, prefix="/user/settings", tags=["user_settings"]
|
user_settings_router, prefix="/user/settings", tags=["user_settings"]
|
||||||
)
|
)
|
||||||
|
router.include_router(diary_router, prefix="/diary", tags=["diary"])
|
||||||
|
router.include_router(meal_router, prefix="/diary/{date}/meal", tags=["meal"])
|
||||||
|
router.include_router(
|
||||||
|
entry_router, prefix="/diary/{date}/meal/{meal_id}/entry", tags=["entry"]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,4 @@ os.environ.update(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
from .fixtures import *
|
from fooder.test.fixtures import *
|
||||||
|
|
|
||||||
15
fooder/test/fixtures/__init__.py
vendored
15
fooder/test/fixtures/__init__.py
vendored
|
|
@ -1,11 +1,12 @@
|
||||||
import pytest
|
import pytest
|
||||||
from .db import *
|
from fooder.test.fixtures.db import *
|
||||||
from .faker import *
|
from fooder.test.fixtures.faker import *
|
||||||
from .user import *
|
from fooder.test.fixtures.user import *
|
||||||
from .client import *
|
from fooder.test.fixtures.client import *
|
||||||
from .context import *
|
from fooder.test.fixtures.context import *
|
||||||
from .product import *
|
from fooder.test.fixtures.product import *
|
||||||
from .user_settings import *
|
from fooder.test.fixtures.user_settings import *
|
||||||
|
from fooder.test.fixtures.diary import *
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
7
fooder/test/fixtures/context.py
vendored
7
fooder/test/fixtures/context.py
vendored
|
|
@ -6,3 +6,10 @@ from fooder.repository import Repository
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ctx(db_session):
|
def ctx(db_session):
|
||||||
return Context(repo=Repository(db_session))
|
return Context(repo=Repository(db_session))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_ctx(db_session, user):
|
||||||
|
ctx = Context(repo=Repository(db_session))
|
||||||
|
ctx.set_user(user)
|
||||||
|
return ctx
|
||||||
|
|
|
||||||
58
fooder/test/fixtures/diary.py
vendored
Normal file
58
fooder/test/fixtures/diary.py
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from fooder.controller.diary import DiaryController
|
||||||
|
from fooder.controller.meal import MealController
|
||||||
|
from fooder.controller.entry import EntryController
|
||||||
|
from fooder.model.meal import MealCreateModel
|
||||||
|
from fooder.model.entry import EntryCreateModel
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def diary(auth_ctx, user_settings):
|
||||||
|
async with auth_ctx.repo.transaction():
|
||||||
|
ctrl = await DiaryController.create(
|
||||||
|
auth_ctx,
|
||||||
|
date=datetime.date.today(),
|
||||||
|
settings=user_settings,
|
||||||
|
)
|
||||||
|
await MealController.create(
|
||||||
|
auth_ctx,
|
||||||
|
diary_id=ctrl.obj.id,
|
||||||
|
data=MealCreateModel(name="Breakfast"),
|
||||||
|
)
|
||||||
|
return ctrl.obj
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def meal(auth_ctx, diary):
|
||||||
|
async with auth_ctx.repo.transaction():
|
||||||
|
ctrl = await MealController.create(
|
||||||
|
auth_ctx,
|
||||||
|
diary_id=diary.id,
|
||||||
|
data=MealCreateModel(name="Lunch"),
|
||||||
|
)
|
||||||
|
return ctrl.obj
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def entry(auth_ctx, meal, product):
|
||||||
|
async with auth_ctx.repo.transaction():
|
||||||
|
ctrl = await EntryController.create(
|
||||||
|
auth_ctx,
|
||||||
|
meal_id=meal.id,
|
||||||
|
data=EntryCreateModel(grams=100.0, product_id=product.id),
|
||||||
|
)
|
||||||
|
return ctrl.obj
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def meal_payload():
|
||||||
|
return {"name": "Dinner"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def entry_payload(product):
|
||||||
|
return {"grams": 150.0, "product_id": product.id}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
from ..fixtures.db import TestModel
|
from fooder.test.fixtures.db import TestModel
|
||||||
from fooder.exc import NotFound
|
from fooder.exc import NotFound
|
||||||
|
|
||||||
# ------------------------------------------------------------------ create ---
|
# ------------------------------------------------------------------ create ---
|
||||||
|
|
|
||||||
120
fooder/test/view/test_diary.py
Normal file
120
fooder/test/view/test_diary.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
TODAY = datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_diary_returns_200(auth_client, diary):
|
||||||
|
response = await auth_client.get(f"/api/diary/{TODAY}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_diary_returns_correct_fields(auth_client, diary, user_settings):
|
||||||
|
response = await auth_client.get(f"/api/diary/{TODAY}")
|
||||||
|
body = response.json()
|
||||||
|
assert body["date"] == TODAY
|
||||||
|
assert body["protein_goal"] == user_settings.protein_goal
|
||||||
|
assert body["calories_goal"] == user_settings.calories_goal
|
||||||
|
assert "meals" in body
|
||||||
|
assert "id" in body
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_diary_not_found_returns_404(auth_client):
|
||||||
|
response = await auth_client.get("/api/diary/2000-01-01")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_diary_without_auth_returns_401(client):
|
||||||
|
response = await client.get(f"/api/diary/{TODAY}")
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_returns_201(auth_client, user_settings):
|
||||||
|
response = await auth_client.post("/api/diary", json={"date": "2030-06-01"})
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_copies_goals_from_user_settings(auth_client, user_settings):
|
||||||
|
response = await auth_client.post("/api/diary", json={"date": "2030-06-02"})
|
||||||
|
body = response.json()
|
||||||
|
assert body["protein_goal"] == user_settings.protein_goal
|
||||||
|
assert body["carb_goal"] == user_settings.carb_goal
|
||||||
|
assert body["fat_goal"] == user_settings.fat_goal
|
||||||
|
assert body["fiber_goal"] == user_settings.fiber_goal
|
||||||
|
assert body["calories_goal"] == user_settings.calories_goal
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_creates_breakfast_meal(auth_client, user_settings):
|
||||||
|
response = await auth_client.post("/api/diary", json={"date": "2030-06-03"})
|
||||||
|
meals = response.json()["meals"]
|
||||||
|
assert len(meals) == 1
|
||||||
|
assert meals[0]["name"] == "Breakfast"
|
||||||
|
assert meals[0]["order"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_without_settings_returns_404(auth_client):
|
||||||
|
response = await auth_client.post("/api/diary", json={"date": "2030-06-04"})
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_duplicate_date_returns_409(auth_client, user_settings):
|
||||||
|
await auth_client.post("/api/diary", json={"date": "2030-06-05"})
|
||||||
|
response = await auth_client.post("/api/diary", json={"date": "2030-06-05"})
|
||||||
|
assert response.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_diary_without_auth_returns_401(client):
|
||||||
|
response = await client.post("/api/diary", json={"date": "2030-06-06"})
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_returns_200(auth_client, diary):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}", json={"protein_goal": 180.0}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_partial_update(auth_client, diary, user_settings):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}", json={"protein_goal": 180.0}
|
||||||
|
)
|
||||||
|
body = response.json()
|
||||||
|
assert body["protein_goal"] == 180.0
|
||||||
|
assert body["carb_goal"] == user_settings.carb_goal
|
||||||
|
assert body["calories_goal"] == user_settings.calories_goal
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_all_goals(auth_client, diary):
|
||||||
|
payload = {
|
||||||
|
"protein_goal": 120.0,
|
||||||
|
"carb_goal": 250.0,
|
||||||
|
"fat_goal": 60.0,
|
||||||
|
"fiber_goal": 25.0,
|
||||||
|
"calories_goal": 1800.0,
|
||||||
|
}
|
||||||
|
response = await auth_client.patch(f"/api/diary/{TODAY}", json=payload)
|
||||||
|
body = response.json()
|
||||||
|
assert body["protein_goal"] == 120.0
|
||||||
|
assert body["carb_goal"] == 250.0
|
||||||
|
assert body["fat_goal"] == 60.0
|
||||||
|
assert body["fiber_goal"] == 25.0
|
||||||
|
assert body["calories_goal"] == 1800.0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_not_found_returns_404(auth_client):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
"/api/diary/2000-01-01", json={"protein_goal": 180.0}
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_negative_goal_returns_422(auth_client, diary):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}", json={"protein_goal": -10.0}
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_diary_without_auth_returns_401(client):
|
||||||
|
response = await client.patch(f"/api/diary/{TODAY}", json={"protein_goal": 180.0})
|
||||||
|
assert response.status_code == 401
|
||||||
113
fooder/test/view/test_entry.py
Normal file
113
fooder/test/view/test_entry.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
TODAY = datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_returns_201(auth_client, diary, meal, entry_payload):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_returns_correct_fields(
|
||||||
|
auth_client, diary, meal, entry_payload, product
|
||||||
|
):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
body = response.json()
|
||||||
|
assert body["grams"] == entry_payload["grams"]
|
||||||
|
assert body["product_id"] == product.id
|
||||||
|
assert body["meal_id"] == meal.id
|
||||||
|
assert "protein" in body
|
||||||
|
assert "calories" in body
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_zero_grams_returns_422(auth_client, diary, meal, product):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry",
|
||||||
|
json={"grams": 0.0, "product_id": product.id},
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_negative_grams_returns_422(
|
||||||
|
auth_client, diary, meal, product
|
||||||
|
):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry",
|
||||||
|
json={"grams": -10.0, "product_id": product.id},
|
||||||
|
)
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_increments_user_product_usage(
|
||||||
|
auth_client, auth_ctx, diary, meal, entry_payload, product
|
||||||
|
):
|
||||||
|
await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
usage = await auth_ctx.repo.user_product_usage.get_by_user_and_product(
|
||||||
|
auth_ctx.user.id, product.id
|
||||||
|
)
|
||||||
|
assert usage.count == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_meal_not_found_returns_404(
|
||||||
|
auth_client, diary, entry_payload
|
||||||
|
):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/99999/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_entry_without_auth_returns_401(
|
||||||
|
client, diary, meal, entry_payload
|
||||||
|
):
|
||||||
|
response = await client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry", json=entry_payload
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_entry_returns_200(auth_client, diary, meal, entry):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry/{entry.id}",
|
||||||
|
json={"grams": 200.0},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_entry_changes_grams(auth_client, diary, meal, entry):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry/{entry.id}",
|
||||||
|
json={"grams": 200.0},
|
||||||
|
)
|
||||||
|
assert response.json()["grams"] == 200.0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_entry_not_found_returns_404(auth_client, diary, meal):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry/99999",
|
||||||
|
json={"grams": 200.0},
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_entry_returns_204(auth_client, diary, meal, entry):
|
||||||
|
response = await auth_client.delete(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry/{entry.id}"
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_entry_without_auth_returns_401(client, diary, meal, entry):
|
||||||
|
response = await client.delete(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}/entry/{entry.id}"
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
73
fooder/test/view/test_meal.py
Normal file
73
fooder/test/view/test_meal.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
TODAY = datetime.date.today().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_returns_201(auth_client, diary, meal_payload):
|
||||||
|
response = await auth_client.post(f"/api/diary/{TODAY}/meal", json=meal_payload)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_auto_increments_order(auth_client, diary, meal_payload):
|
||||||
|
r1 = await auth_client.post(f"/api/diary/{TODAY}/meal", json=meal_payload)
|
||||||
|
r2 = await auth_client.post(f"/api/diary/{TODAY}/meal", json=meal_payload)
|
||||||
|
assert r2.json()["order"] == r1.json()["order"] + 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_first_order_is_one(auth_client, diary, meal_payload):
|
||||||
|
# diary fixture already creates a Breakfast meal with order=0
|
||||||
|
response = await auth_client.post(f"/api/diary/{TODAY}/meal", json=meal_payload)
|
||||||
|
assert response.json()["order"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_uses_provided_order(auth_client, diary, meal_payload):
|
||||||
|
response = await auth_client.post(
|
||||||
|
f"/api/diary/{TODAY}/meal", json={**meal_payload, "order": 99}
|
||||||
|
)
|
||||||
|
assert response.json()["order"] == 99
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_diary_not_found_returns_404(auth_client, meal_payload):
|
||||||
|
response = await auth_client.post("/api/diary/2000-01-01/meal", json=meal_payload)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_meal_without_auth_returns_401(client, diary, meal_payload):
|
||||||
|
response = await client.post(f"/api/diary/{TODAY}/meal", json=meal_payload)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_meal_returns_200(auth_client, diary, meal):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}", json={"name": "Supper"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_meal_changes_name(auth_client, diary, meal):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/{meal.id}", json={"name": "Supper"}
|
||||||
|
)
|
||||||
|
assert response.json()["name"] == "Supper"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_meal_not_found_returns_404(auth_client, diary):
|
||||||
|
response = await auth_client.patch(
|
||||||
|
f"/api/diary/{TODAY}/meal/99999", json={"name": "Ghost"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_meal_returns_204(auth_client, diary, meal):
|
||||||
|
response = await auth_client.delete(f"/api/diary/{TODAY}/meal/{meal.id}")
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_meal_not_found_returns_404(auth_client, diary):
|
||||||
|
response = await auth_client.delete(f"/api/diary/{TODAY}/meal/99999")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_delete_meal_without_auth_returns_401(client, diary, meal):
|
||||||
|
response = await client.delete(f"/api/diary/{TODAY}/meal/{meal.id}")
|
||||||
|
assert response.status_code == 401
|
||||||
36
fooder/view/diary.py
Normal file
36
fooder/view/diary.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from fooder.command.create_diary import create_diary
|
||||||
|
from fooder.context import AuthContextDependency, Context
|
||||||
|
from fooder.controller.diary import DiaryController
|
||||||
|
from fooder.model.diary import DiaryCreateModel, DiaryModel, DiaryUpdateModel
|
||||||
|
|
||||||
|
router = APIRouter(tags=["diary"])
|
||||||
|
|
||||||
|
_auth_ctx = AuthContextDependency()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{date}", response_model=DiaryModel)
|
||||||
|
async def get_diary(date: datetime.date, ctx: Context = Depends(_auth_ctx)):
|
||||||
|
return await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{date}", response_model=DiaryModel)
|
||||||
|
async def update_diary(
|
||||||
|
date: datetime.date,
|
||||||
|
data: DiaryUpdateModel,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
await DiaryController(ctx, diary).update(data)
|
||||||
|
return diary
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=DiaryModel, status_code=201)
|
||||||
|
async def create_diary_route(data: DiaryCreateModel, ctx: Context = Depends(_auth_ctx)):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await create_diary(ctx, date=data.date)
|
||||||
|
return diary
|
||||||
55
fooder/view/entry.py
Normal file
55
fooder/view/entry.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from fooder.command.create_entry import create_entry
|
||||||
|
from fooder.context import AuthContextDependency, Context
|
||||||
|
from fooder.controller.entry import EntryController
|
||||||
|
from fooder.model.entry import EntryCreateModel, EntryModel, EntryUpdateModel
|
||||||
|
|
||||||
|
router = APIRouter(tags=["entry"])
|
||||||
|
|
||||||
|
_auth_ctx = AuthContextDependency()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=EntryModel, status_code=201)
|
||||||
|
async def create_entry_route(
|
||||||
|
date: datetime.date,
|
||||||
|
meal_id: int,
|
||||||
|
data: EntryCreateModel,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
await ctx.repo.meal.get_by_id_and_diary(meal_id, diary.id)
|
||||||
|
entry = await create_entry(ctx, meal_id=meal_id, data=data)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{entry_id}", response_model=EntryModel)
|
||||||
|
async def update_entry(
|
||||||
|
date: datetime.date,
|
||||||
|
meal_id: int,
|
||||||
|
entry_id: int,
|
||||||
|
data: EntryUpdateModel,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
await ctx.repo.meal.get_by_id_and_diary(meal_id, diary.id)
|
||||||
|
entry = await ctx.repo.entry.get_by_id_and_meal(entry_id, meal_id)
|
||||||
|
await EntryController(ctx, entry).update(data)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{entry_id}", status_code=204)
|
||||||
|
async def delete_entry(
|
||||||
|
date: datetime.date,
|
||||||
|
meal_id: int,
|
||||||
|
entry_id: int,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
await ctx.repo.meal.get_by_id_and_diary(meal_id, diary.id)
|
||||||
|
await ctx.repo.entry.delete_by_id_and_meal(entry_id, meal_id)
|
||||||
49
fooder/view/meal.py
Normal file
49
fooder/view/meal.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from fooder.context import AuthContextDependency, Context
|
||||||
|
from fooder.controller.meal import MealController
|
||||||
|
from fooder.model.meal import MealCreateModel, MealModel, MealUpdateModel
|
||||||
|
|
||||||
|
router = APIRouter(tags=["meal"])
|
||||||
|
|
||||||
|
_auth_ctx = AuthContextDependency()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=MealModel, status_code=201)
|
||||||
|
async def create_meal(
|
||||||
|
date: datetime.date,
|
||||||
|
data: MealCreateModel,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
ctrl = await MealController.create(ctx, diary_id=diary.id, data=data)
|
||||||
|
return ctrl.obj
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{meal_id}", response_model=MealModel)
|
||||||
|
async def update_meal(
|
||||||
|
date: datetime.date,
|
||||||
|
meal_id: int,
|
||||||
|
data: MealUpdateModel,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
meal = await ctx.repo.meal.get_by_id_and_diary(meal_id, diary.id)
|
||||||
|
await MealController(ctx, meal).update(data)
|
||||||
|
return meal
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{meal_id}", status_code=204)
|
||||||
|
async def delete_meal(
|
||||||
|
date: datetime.date,
|
||||||
|
meal_id: int,
|
||||||
|
ctx: Context = Depends(_auth_ctx),
|
||||||
|
):
|
||||||
|
async with ctx.repo.transaction():
|
||||||
|
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||||
|
await ctx.repo.meal.get_by_id_and_diary(meal_id, diary.id)
|
||||||
|
await ctx.repo.meal.delete_by_id_and_diary(meal_id, diary.id)
|
||||||
Loading…
Reference in a new issue