[preset] implement [repo] simplify fetching for views on most repos
This commit is contained in:
parent
444893e1fd
commit
65d0a19e41
30 changed files with 862 additions and 66 deletions
|
|
@ -20,6 +20,10 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
_now = sa.text("CURRENT_TIMESTAMP")
|
||||
_zero_int = sa.text("0")
|
||||
_zero_float = sa.text("0")
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"userproductusage",
|
||||
|
|
@ -60,30 +64,111 @@ def upgrade() -> None:
|
|||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id"),
|
||||
)
|
||||
# Create default settings for all existing users
|
||||
op.execute(
|
||||
sa.text(
|
||||
"INSERT INTO usersettings"
|
||||
" (user_id, protein_goal, carb_goal, fat_goal, fiber_goal, calories_goal,"
|
||||
" version, created_at, last_changed)"
|
||||
" SELECT id, 0, 0, 0, 0, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP"
|
||||
' FROM "user"'
|
||||
)
|
||||
)
|
||||
|
||||
op.drop_table("refreshtoken")
|
||||
op.add_column("diary", sa.Column("protein_goal", sa.Float(), nullable=False))
|
||||
op.add_column("diary", sa.Column("carb_goal", sa.Float(), nullable=False))
|
||||
op.add_column("diary", sa.Column("fat_goal", sa.Float(), nullable=False))
|
||||
op.add_column("diary", sa.Column("fiber_goal", sa.Float(), nullable=False))
|
||||
op.add_column("diary", sa.Column("calories_goal", sa.Float(), nullable=False))
|
||||
op.add_column("diary", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("diary", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("diary", sa.Column("last_changed", sa.DateTime(), nullable=False))
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column(
|
||||
"protein_goal", sa.Float(), nullable=False, server_default=_zero_float
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("carb_goal", sa.Float(), nullable=False, server_default=_zero_float),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("fat_goal", sa.Float(), nullable=False, server_default=_zero_float),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("fiber_goal", sa.Float(), nullable=False, server_default=_zero_float),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column(
|
||||
"calories_goal", sa.Float(), nullable=False, server_default=_zero_float
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"diary",
|
||||
sa.Column("last_changed", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.create_unique_constraint(None, "diary", ["user_id", "date"])
|
||||
op.add_column("entry", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("entry", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("meal", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("meal", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("meal", sa.Column("last_changed", sa.DateTime(), nullable=False))
|
||||
op.add_column("preset", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("preset", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("preset", sa.Column("last_changed", sa.DateTime(), nullable=False))
|
||||
op.add_column("presetentry", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("presetentry", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("product", sa.Column("calories", sa.Float(), nullable=False))
|
||||
op.add_column("product", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("product", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("product", sa.Column("last_changed", sa.DateTime(), nullable=False))
|
||||
op.add_column(
|
||||
"entry",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"entry",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"meal",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"meal",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"meal",
|
||||
sa.Column("last_changed", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"preset",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"preset",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"preset",
|
||||
sa.Column("last_changed", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"presetentry",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"presetentry",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"product",
|
||||
sa.Column("calories", sa.Float(), nullable=False, server_default=_zero_float),
|
||||
)
|
||||
op.add_column(
|
||||
"product",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"product",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"product",
|
||||
sa.Column("last_changed", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column("product", sa.Column("deleted_at", sa.DateTime(), nullable=True))
|
||||
op.create_index(
|
||||
"ix_product_barcode",
|
||||
|
|
@ -95,9 +180,18 @@ def upgrade() -> None:
|
|||
)
|
||||
op.drop_column("product", "hard_coded_calories")
|
||||
op.drop_column("product", "usage_count_cached")
|
||||
op.add_column("user", sa.Column("version", sa.Integer(), nullable=False))
|
||||
op.add_column("user", sa.Column("created_at", sa.DateTime(), nullable=False))
|
||||
op.add_column("user", sa.Column("last_changed", sa.DateTime(), nullable=False))
|
||||
op.add_column(
|
||||
"user",
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default=_zero_int),
|
||||
)
|
||||
op.add_column(
|
||||
"user",
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column(
|
||||
"user",
|
||||
sa.Column("last_changed", sa.DateTime(), nullable=False, server_default=_now),
|
||||
)
|
||||
op.add_column("user", sa.Column("deleted_at", sa.DateTime(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
|
|||
71
fooder/alembic/versions/564e5948f3ed_.py
Normal file
71
fooder/alembic/versions/564e5948f3ed_.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
|
||||
Revision ID: 564e5948f3ed
|
||||
Revises: 4e8d78ff6e9e
|
||||
Create Date: 2026-04-07 19:31:01.616100
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "564e5948f3ed"
|
||||
down_revision: Union[str, Sequence[str], None] = "4e8d78ff6e9e"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(op.f("ix_entry_meal_id"), "entry", ["meal_id"], unique=False)
|
||||
op.create_index(op.f("ix_entry_product_id"), "entry", ["product_id"], unique=False)
|
||||
op.drop_column("entry", "processed")
|
||||
op.create_index(op.f("ix_meal_diary_id"), "meal", ["diary_id"], unique=False)
|
||||
op.create_index(op.f("ix_preset_user_id"), "preset", ["user_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_presetentry_preset_id"),
|
||||
"presetentry",
|
||||
["preset_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_presetentry_product_id"),
|
||||
"presetentry",
|
||||
["product_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_userproductusage_product_user",
|
||||
"userproductusage",
|
||||
["product_id", "user_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_userproductusage_user_id"),
|
||||
"userproductusage",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_userproductusage_user_id"), table_name="userproductusage")
|
||||
op.drop_index("ix_userproductusage_product_user", table_name="userproductusage")
|
||||
op.drop_index(op.f("ix_presetentry_product_id"), table_name="presetentry")
|
||||
op.drop_index(op.f("ix_presetentry_preset_id"), table_name="presetentry")
|
||||
op.drop_index(op.f("ix_preset_user_id"), table_name="preset")
|
||||
op.drop_index(op.f("ix_meal_diary_id"), table_name="meal")
|
||||
op.add_column(
|
||||
"entry",
|
||||
sa.Column("processed", sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||
)
|
||||
op.drop_index(op.f("ix_entry_product_id"), table_name="entry")
|
||||
op.drop_index(op.f("ix_entry_meal_id"), table_name="entry")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.entry import EntryController
|
||||
from fooder.domain import Entry
|
||||
from fooder.domain.meal import Meal
|
||||
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)
|
||||
async def create_entry(ctx: Context, meal: Meal, data: EntryCreateModel) -> Entry:
|
||||
ctrl = await EntryController.create(ctx, meal=meal, data=data)
|
||||
await ctx.repo.user_product_usage.increment(
|
||||
user_id=ctx.user.id,
|
||||
product_id=data.product_id,
|
||||
|
|
|
|||
22
fooder/command/load_preset_as_meal.py
Normal file
22
fooder/command/load_preset_as_meal.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.entry import EntryController
|
||||
from fooder.controller.meal import MealController
|
||||
from fooder.domain import Meal, Preset
|
||||
from fooder.model.entry import EntryCreateModel
|
||||
from fooder.model.meal import MealCreateModel
|
||||
|
||||
|
||||
async def load_preset_as_meal(
|
||||
ctx: Context, preset: Preset, diary_id: int, name: str | None = None
|
||||
) -> Meal:
|
||||
meal_ctrl = await MealController.create(
|
||||
ctx, diary_id=diary_id, data=MealCreateModel(name=name or preset.name)
|
||||
)
|
||||
for entry in preset.entries:
|
||||
await EntryController.create(
|
||||
ctx,
|
||||
meal=meal_ctrl.obj,
|
||||
data=EntryCreateModel(grams=entry.grams, product_id=entry.product_id),
|
||||
)
|
||||
await ctx.repo.meal.session.refresh(meal_ctrl.obj)
|
||||
return meal_ctrl.obj
|
||||
19
fooder/command/save_meal_as_preset.py
Normal file
19
fooder/command/save_meal_as_preset.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.preset import PresetController
|
||||
from fooder.domain import Meal, Preset, PresetEntry
|
||||
|
||||
|
||||
async def save_meal_as_preset(
|
||||
ctx: Context, meal: Meal, name: str | None = None
|
||||
) -> Preset:
|
||||
await ctx.repo.meal.session.refresh(meal)
|
||||
ctrl = await PresetController.create(ctx, name=name or meal.name)
|
||||
for entry in meal.entries:
|
||||
preset_entry = PresetEntry(
|
||||
grams=entry.grams,
|
||||
product_id=entry.product_id,
|
||||
preset_id=ctrl.obj.id,
|
||||
)
|
||||
await ctx.repo.preset_entry.create(preset_entry)
|
||||
await ctx.repo.preset.session.refresh(ctrl.obj)
|
||||
return ctrl.obj
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.base import ModelController
|
||||
from fooder.domain import Entry
|
||||
from fooder.domain.meal import Meal
|
||||
from fooder.model.entry import EntryCreateModel, EntryUpdateModel
|
||||
|
||||
|
||||
class EntryController(ModelController[Entry]):
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, ctx: Context, meal_id: int, data: EntryCreateModel
|
||||
cls, ctx: Context, meal: Meal, data: EntryCreateModel
|
||||
) -> "EntryController":
|
||||
obj = Entry(grams=data.grams, product_id=data.product_id, meal_id=meal_id)
|
||||
obj = Entry(grams=data.grams, product_id=data.product_id)
|
||||
obj.meal = meal
|
||||
await ctx.repo.entry.create(obj)
|
||||
return cls(ctx, obj)
|
||||
|
||||
|
|
|
|||
17
fooder/controller/preset.py
Normal file
17
fooder/controller/preset.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.base import ModelController
|
||||
from fooder.domain import Preset
|
||||
from fooder.model.preset import PresetUpdateModel
|
||||
|
||||
|
||||
class PresetController(ModelController[Preset]):
|
||||
@classmethod
|
||||
async def create(cls, ctx: Context, name: str) -> "PresetController":
|
||||
obj = Preset(name=name, user_id=ctx.user.id)
|
||||
await ctx.repo.preset.create(obj)
|
||||
return cls(ctx, obj)
|
||||
|
||||
async def update(self, data: PresetUpdateModel) -> None:
|
||||
if data.name is not None:
|
||||
self.obj.name = data.name
|
||||
await self.ctx.repo.preset.update(self.obj)
|
||||
21
fooder/controller/preset_entry.py
Normal file
21
fooder/controller/preset_entry.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from fooder.context import Context
|
||||
from fooder.controller.base import ModelController
|
||||
from fooder.domain import PresetEntry
|
||||
from fooder.domain.preset import Preset
|
||||
from fooder.model.entry import EntryCreateModel, EntryUpdateModel
|
||||
|
||||
|
||||
class PresetEntryController(ModelController[PresetEntry]):
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, ctx: Context, preset: Preset, data: EntryCreateModel
|
||||
) -> "PresetEntryController":
|
||||
obj = PresetEntry(grams=data.grams, product_id=data.product_id)
|
||||
obj.preset = preset
|
||||
await ctx.repo.preset_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.preset_entry.update(self.obj)
|
||||
|
|
@ -1,14 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from fooder.domain.base import Base, CommonMixin, EntryMacrosMixin
|
||||
from fooder.domain.product import Product
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fooder.domain.meal import Meal
|
||||
|
||||
|
||||
class Entry(Base, CommonMixin, EntryMacrosMixin):
|
||||
"""Entry."""
|
||||
|
||||
grams: Mapped[float]
|
||||
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("product.id"), index=True
|
||||
)
|
||||
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"), index=True)
|
||||
meal: Mapped[Meal] = relationship(back_populates="entries")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ class Meal(Base, CommonMixin, AggregateMacrosMixin):
|
|||
|
||||
name: Mapped[str]
|
||||
order: Mapped[int]
|
||||
diary_id: Mapped[int] = mapped_column(Integer, ForeignKey("diary.id"))
|
||||
diary_id: Mapped[int] = mapped_column(Integer, ForeignKey("diary.id"), index=True)
|
||||
entries: Mapped[list[Entry]] = relationship(
|
||||
lazy="selectin", order_by=Entry.last_changed, cascade="all, delete-orphan"
|
||||
lazy="selectin",
|
||||
order_by=Entry.last_changed,
|
||||
cascade="all, delete-orphan",
|
||||
back_populates="meal",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ class Preset(Base, CommonMixin, AggregateMacrosMixin):
|
|||
"""Preset."""
|
||||
|
||||
name: Mapped[str]
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"))
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"), index=True)
|
||||
entries: Mapped[list[PresetEntry]] = relationship(
|
||||
lazy="selectin", order_by=PresetEntry.last_changed
|
||||
lazy="selectin",
|
||||
order_by=PresetEntry.last_changed,
|
||||
cascade="all, delete-orphan",
|
||||
back_populates="preset",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from fooder.domain.base import Base, CommonMixin, EntryMacrosMixin
|
||||
from fooder.domain.product import Product
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fooder.domain.preset import Preset
|
||||
|
||||
|
||||
class PresetEntry(Base, CommonMixin, EntryMacrosMixin):
|
||||
"""PresetEntry."""
|
||||
|
||||
grams: Mapped[float]
|
||||
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("product.id"), index=True
|
||||
)
|
||||
product: Mapped[Product] = relationship(lazy="selectin")
|
||||
preset_id: Mapped[int] = mapped_column(Integer, ForeignKey("preset.id"))
|
||||
preset_id: Mapped[int] = mapped_column(Integer, ForeignKey("preset.id"), index=True)
|
||||
preset: Mapped[Preset] = relationship(back_populates="entries")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from sqlalchemy import ForeignKey, Integer, UniqueConstraint
|
||||
from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from fooder.domain.base import Base, CommonMixin
|
||||
|
|
@ -10,10 +10,14 @@ class UserProductUsage(Base, CommonMixin):
|
|||
"""Counts how many processed entries a user has for a product.
|
||||
Used to sort products by usage frequency."""
|
||||
|
||||
__table_args__ = (UniqueConstraint("user_id", "product_id"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "product_id"),
|
||||
# Covers outerjoin on (product_id, user_id) in list_for_user
|
||||
Index("ix_userproductusage_product_user", "product_id", "user_id"),
|
||||
)
|
||||
|
||||
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("product.id"))
|
||||
product: Mapped[Product] = relationship(lazy="selectin")
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"))
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"), index=True)
|
||||
user: Mapped[User] = relationship(lazy="selectin")
|
||||
count: Mapped[int]
|
||||
|
|
|
|||
28
fooder/model/preset.py
Normal file
28
fooder/model/preset.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from fooder.model.base import ObjModelMixin
|
||||
from fooder.model.preset_entry import PresetEntryModel
|
||||
|
||||
|
||||
class PresetModel(ObjModelMixin, BaseModel):
|
||||
name: str
|
||||
user_id: int
|
||||
protein: float
|
||||
carb: float
|
||||
fat: float
|
||||
fiber: float
|
||||
calories: float
|
||||
entries: list[PresetEntryModel]
|
||||
|
||||
|
||||
class SaveAsPresetModel(BaseModel):
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class PresetUpdateModel(BaseModel):
|
||||
name: str | None = None
|
||||
|
||||
|
||||
class LoadPresetAsMealModel(BaseModel):
|
||||
preset_id: int
|
||||
name: str | None = None
|
||||
17
fooder/model/preset_entry.py
Normal file
17
fooder/model/preset_entry.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from fooder.model.base import ObjModelMixin
|
||||
from fooder.model.entry import Grams
|
||||
from fooder.model.product import ProductModel
|
||||
|
||||
|
||||
class PresetEntryModel(ObjModelMixin, BaseModel):
|
||||
grams: Grams
|
||||
product_id: int
|
||||
preset_id: int
|
||||
product: ProductModel
|
||||
protein: float
|
||||
carb: float
|
||||
fat: float
|
||||
fiber: float
|
||||
calories: float
|
||||
|
|
@ -5,3 +5,7 @@ class TokenResponse(BaseModel):
|
|||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
from sqlalchemy import select
|
||||
|
||||
from fooder.domain import Entry
|
||||
from fooder.domain.diary import Diary
|
||||
from fooder.domain.meal import Meal
|
||||
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 get_by_id_and_user(self, entry_id: int, user_id: int) -> Entry:
|
||||
stmt = (
|
||||
select(Entry)
|
||||
.join(Meal, Entry.meal_id == Meal.id)
|
||||
.join(Diary, Meal.diary_id == Diary.id)
|
||||
.where(Entry.id == entry_id, Diary.user_id == user_id)
|
||||
)
|
||||
return await self._get(stmt=stmt)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
from sqlalchemy import select, func
|
||||
|
||||
from fooder.domain import Meal
|
||||
from fooder.domain.diary import Diary
|
||||
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 get_by_id_and_user(self, meal_id: int, user_id: int) -> Meal:
|
||||
stmt = (
|
||||
select(Meal)
|
||||
.join(Diary, Meal.diary_id == Diary.id)
|
||||
.where(Meal.id == meal_id, Diary.user_id == user_id)
|
||||
)
|
||||
return await self._get(stmt=stmt)
|
||||
|
||||
async def next_order(self, diary_id: int) -> int:
|
||||
stmt = select(func.max(Meal.order)).where(Meal.diary_id == diary_id)
|
||||
|
|
|
|||
17
fooder/repository/preset.py
Normal file
17
fooder/repository/preset.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from typing import Sequence
|
||||
|
||||
from fooder.domain import Preset
|
||||
from fooder.repository.base import DEFAULT_LIMIT, RepositoryBase
|
||||
|
||||
|
||||
class PresetRepository(RepositoryBase[Preset]):
|
||||
async def get_by_id_and_user(self, preset_id: int, user_id: int) -> Preset:
|
||||
return await self._get(Preset.id == preset_id, Preset.user_id == user_id)
|
||||
|
||||
async def list_by_user(
|
||||
self,
|
||||
user_id: int,
|
||||
offset: int = 0,
|
||||
limit: int | None = DEFAULT_LIMIT,
|
||||
) -> Sequence[Preset]:
|
||||
return await self._list(Preset.user_id == user_id, offset=offset, limit=limit)
|
||||
15
fooder/repository/preset_entry.py
Normal file
15
fooder/repository/preset_entry.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from sqlalchemy import select
|
||||
|
||||
from fooder.domain import PresetEntry
|
||||
from fooder.domain.preset import Preset
|
||||
from fooder.repository.base import RepositoryBase
|
||||
|
||||
|
||||
class PresetEntryRepository(RepositoryBase[PresetEntry]):
|
||||
async def get_by_id_and_user(self, entry_id: int, user_id: int) -> PresetEntry:
|
||||
stmt = (
|
||||
select(PresetEntry)
|
||||
.join(Preset, PresetEntry.preset_id == Preset.id)
|
||||
.where(PresetEntry.id == entry_id, Preset.user_id == user_id)
|
||||
)
|
||||
return await self._get(stmt=stmt)
|
||||
|
|
@ -10,6 +10,8 @@ from fooder.repository.user_settings import UserSettingsRepository
|
|||
from fooder.repository.diary import DiaryRepository
|
||||
from fooder.repository.meal import MealRepository
|
||||
from fooder.repository.entry import EntryRepository
|
||||
from fooder.repository.preset import PresetRepository
|
||||
from fooder.repository.preset_entry import PresetEntryRepository
|
||||
from fooder.domain import (
|
||||
User,
|
||||
Product,
|
||||
|
|
@ -18,6 +20,8 @@ from fooder.domain import (
|
|||
Diary,
|
||||
Meal,
|
||||
Entry,
|
||||
Preset,
|
||||
PresetEntry,
|
||||
)
|
||||
from fooder.exc import Conflict
|
||||
|
||||
|
|
@ -32,6 +36,8 @@ class Repository:
|
|||
self.diary = DiaryRepository(Diary, session)
|
||||
self.meal = MealRepository(Meal, session)
|
||||
self.entry = EntryRepository(Entry, session)
|
||||
self.preset = PresetRepository(Preset, session)
|
||||
self.preset_entry = PresetEntryRepository(PresetEntry, session)
|
||||
|
||||
async def commit(self) -> None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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
|
||||
from fooder.view.preset import router as preset_router
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
router.include_router(token_router, prefix="/token", tags=["token"])
|
||||
|
|
@ -18,3 +19,4 @@ 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"]
|
||||
)
|
||||
router.include_router(preset_router, prefix="/preset", tags=["preset"])
|
||||
|
|
|
|||
2
fooder/test/fixtures/diary.py
vendored
2
fooder/test/fixtures/diary.py
vendored
|
|
@ -42,7 +42,7 @@ async def entry(auth_ctx, meal, product):
|
|||
async with auth_ctx.repo.transaction():
|
||||
ctrl = await EntryController.create(
|
||||
auth_ctx,
|
||||
meal_id=meal.id,
|
||||
meal=meal,
|
||||
data=EntryCreateModel(grams=100.0, product_id=product.id),
|
||||
)
|
||||
return ctrl.obj
|
||||
|
|
|
|||
285
fooder/test/view/test_preset.py
Normal file
285
fooder/test/view/test_preset.py
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import datetime
|
||||
|
||||
import pytest_asyncio
|
||||
|
||||
from fooder.command.save_meal_as_preset import save_meal_as_preset
|
||||
|
||||
TODAY = datetime.date.today().isoformat()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def preset(auth_ctx, meal, entry):
|
||||
async with auth_ctx.repo.transaction():
|
||||
obj = await save_meal_as_preset(auth_ctx, meal)
|
||||
await auth_ctx.repo.preset.session.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def preset_entry(preset):
|
||||
return preset.entries[0]
|
||||
|
||||
|
||||
# --- save meal as preset ---
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_returns_201(auth_client, meal, entry):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_returns_preset_name(auth_client, meal, entry):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={}
|
||||
)
|
||||
assert response.json()["name"] == meal.name
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_overrides_name(auth_client, meal, entry):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={"name": "My Custom Preset"}
|
||||
)
|
||||
assert response.json()["name"] == "My Custom Preset"
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_copies_entries(auth_client, meal, entry):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={}
|
||||
)
|
||||
body = response.json()
|
||||
assert len(body["entries"]) == 1
|
||||
assert body["entries"][0]["grams"] == entry.grams
|
||||
assert body["entries"][0]["product_id"] == entry.product_id
|
||||
|
||||
|
||||
async def test_save_empty_meal_as_preset_has_no_entries(auth_client, meal):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["entries"] == []
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_meal_not_found_returns_404(auth_client, diary):
|
||||
response = await auth_client.post(f"/api/diary/{TODAY}/meal/99999/preset", json={})
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_save_meal_as_preset_without_auth_returns_401(client, meal):
|
||||
response = await client.post(f"/api/diary/{TODAY}/meal/{meal.id}/preset", json={})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- list presets ---
|
||||
|
||||
|
||||
async def test_list_presets_returns_200(auth_client):
|
||||
response = await auth_client.get("/api/preset")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
|
||||
async def test_list_presets_contains_saved(auth_client, preset):
|
||||
response = await auth_client.get("/api/preset")
|
||||
ids = [p["id"] for p in response.json()]
|
||||
assert preset.id in ids
|
||||
|
||||
|
||||
async def test_list_presets_without_auth_returns_401(client):
|
||||
response = await client.get("/api/preset")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- update preset ---
|
||||
|
||||
|
||||
async def test_update_preset_returns_200(auth_client, preset):
|
||||
response = await auth_client.patch(
|
||||
f"/api/preset/{preset.id}", json={"name": "Renamed"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_update_preset_changes_name(auth_client, preset):
|
||||
response = await auth_client.patch(
|
||||
f"/api/preset/{preset.id}", json={"name": "Renamed"}
|
||||
)
|
||||
assert response.json()["name"] == "Renamed"
|
||||
|
||||
|
||||
async def test_update_preset_not_found_returns_404(auth_client):
|
||||
response = await auth_client.patch("/api/preset/99999", json={"name": "Ghost"})
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_update_preset_without_auth_returns_401(client, preset):
|
||||
response = await client.patch(f"/api/preset/{preset.id}", json={"name": "x"})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- delete preset ---
|
||||
|
||||
|
||||
async def test_delete_preset_returns_204(auth_client, preset):
|
||||
response = await auth_client.delete(f"/api/preset/{preset.id}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
async def test_delete_preset_not_found_returns_404(auth_client):
|
||||
response = await auth_client.delete("/api/preset/99999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_delete_preset_without_auth_returns_401(client, preset):
|
||||
response = await client.delete(f"/api/preset/{preset.id}")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- create preset entry ---
|
||||
|
||||
|
||||
async def test_create_preset_entry_returns_201(auth_client, preset, product):
|
||||
response = await auth_client.post(
|
||||
f"/api/preset/{preset.id}/entry",
|
||||
json={"grams": 200.0, "product_id": product.id},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
async def test_create_preset_entry_returns_correct_grams(auth_client, preset, product):
|
||||
response = await auth_client.post(
|
||||
f"/api/preset/{preset.id}/entry",
|
||||
json={"grams": 200.0, "product_id": product.id},
|
||||
)
|
||||
assert response.json()["grams"] == 200.0
|
||||
|
||||
|
||||
async def test_create_preset_entry_preset_not_found_returns_404(auth_client, product):
|
||||
response = await auth_client.post(
|
||||
"/api/preset/99999/entry", json={"grams": 100.0, "product_id": product.id}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_create_preset_entry_without_auth_returns_401(client, preset, product):
|
||||
response = await client.post(
|
||||
f"/api/preset/{preset.id}/entry",
|
||||
json={"grams": 100.0, "product_id": product.id},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- update preset entry ---
|
||||
|
||||
|
||||
async def test_update_preset_entry_returns_200(auth_client, preset, preset_entry):
|
||||
response = await auth_client.patch(
|
||||
f"/api/preset/{preset.id}/entry/{preset_entry.id}",
|
||||
json={"grams": 250.0},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
async def test_update_preset_entry_changes_grams(auth_client, preset, preset_entry):
|
||||
response = await auth_client.patch(
|
||||
f"/api/preset/{preset.id}/entry/{preset_entry.id}",
|
||||
json={"grams": 250.0},
|
||||
)
|
||||
assert response.json()["grams"] == 250.0
|
||||
|
||||
|
||||
async def test_update_preset_entry_not_found_returns_404(auth_client, preset):
|
||||
response = await auth_client.patch(
|
||||
f"/api/preset/{preset.id}/entry/99999", json={"grams": 100.0}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_update_preset_entry_without_auth_returns_401(
|
||||
client, preset, preset_entry
|
||||
):
|
||||
response = await client.patch(
|
||||
f"/api/preset/{preset.id}/entry/{preset_entry.id}",
|
||||
json={"grams": 100.0},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- delete preset entry ---
|
||||
|
||||
|
||||
async def test_delete_preset_entry_returns_204(auth_client, preset, preset_entry):
|
||||
response = await auth_client.delete(
|
||||
f"/api/preset/{preset.id}/entry/{preset_entry.id}"
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
async def test_delete_preset_entry_not_found_returns_404(auth_client, preset):
|
||||
response = await auth_client.delete(f"/api/preset/{preset.id}/entry/99999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_delete_preset_entry_without_auth_returns_401(
|
||||
client, preset, preset_entry
|
||||
):
|
||||
response = await client.delete(f"/api/preset/{preset.id}/entry/{preset_entry.id}")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# --- load preset as meal ---
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_returns_201(auth_client, diary, preset):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset", json={"preset_id": preset.id}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_uses_preset_name(auth_client, diary, preset):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset", json={"preset_id": preset.id}
|
||||
)
|
||||
assert response.json()["name"] == preset.name
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_overrides_name(auth_client, diary, preset):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset",
|
||||
json={"preset_id": preset.id, "name": "Custom Name"},
|
||||
)
|
||||
assert response.json()["name"] == "Custom Name"
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_copies_entries(auth_client, diary, preset):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset", json={"preset_id": preset.id}
|
||||
)
|
||||
body = response.json()
|
||||
assert len(body["entries"]) == len(preset.entries)
|
||||
assert body["entries"][0]["grams"] == preset.entries[0].grams
|
||||
assert body["entries"][0]["product_id"] == preset.entries[0].product_id
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_diary_not_found_returns_404(auth_client, preset):
|
||||
response = await auth_client.post(
|
||||
"/api/diary/2000-01-01/meal/from_preset", json={"preset_id": preset.id}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_preset_not_found_returns_404(auth_client, diary):
|
||||
response = await auth_client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset", json={"preset_id": 99999}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
async def test_load_preset_as_meal_without_auth_returns_401(client, preset):
|
||||
response = await client.post(
|
||||
f"/api/diary/{TODAY}/meal/from_preset", json={"preset_id": preset.id}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
|
@ -1,4 +1,30 @@
|
|||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from fooder.controller.product import ProductController
|
||||
from fooder.model.product import ProductCreateModel
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def second_product(ctx):
|
||||
data = ProductCreateModel(
|
||||
name="Broccoli", protein=2.8, carb=7.0, fat=0.4, fiber=2.6
|
||||
)
|
||||
async with ctx.repo.transaction():
|
||||
ctrl = await ProductController.create(ctx, data)
|
||||
return ctrl.obj
|
||||
|
||||
|
||||
async def test_list_products_orders_by_usage(
|
||||
auth_client, auth_ctx, product, second_product
|
||||
):
|
||||
async with auth_ctx.repo.transaction():
|
||||
await auth_ctx.repo.user_product_usage.increment(
|
||||
user_id=auth_ctx.user.id, product_id=second_product.id, count=5
|
||||
)
|
||||
response = await auth_client.get("/api/product")
|
||||
ids = [p["id"] for p in response.json()]
|
||||
assert ids.index(second_product.id) < ids.index(product.id)
|
||||
|
||||
|
||||
async def test_update_product_returns_200(auth_client, product):
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ async def test_refresh_token_returns_new_tokens(client, user, user_password):
|
|||
refresh_token = response.json()["refresh_token"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/token/refresh", params={"refresh_token": refresh_token}
|
||||
"/api/token/refresh", json={"refresh_token": refresh_token}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
|
@ -72,7 +72,7 @@ async def test_refresh_token_access_token_is_valid(client, user, user_password):
|
|||
refresh_token = response.json()["refresh_token"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/token/refresh", params={"refresh_token": refresh_token}
|
||||
"/api/token/refresh", json={"refresh_token": refresh_token}
|
||||
)
|
||||
token = AccessToken.decode(response.json()["access_token"])
|
||||
assert token.sub == user.id
|
||||
|
|
@ -86,7 +86,7 @@ async def test_refresh_token_refresh_token_is_valid(client, user, user_password)
|
|||
refresh_token = response.json()["refresh_token"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/token/refresh", params={"refresh_token": refresh_token}
|
||||
"/api/token/refresh", json={"refresh_token": refresh_token}
|
||||
)
|
||||
token = RefreshToken.decode(response.json()["refresh_token"])
|
||||
assert token.sub == user.id
|
||||
|
|
@ -94,7 +94,7 @@ async def test_refresh_token_refresh_token_is_valid(client, user, user_password)
|
|||
|
||||
async def test_refresh_token_invalid_returns_401(client):
|
||||
response = await client.post(
|
||||
"/api/token/refresh", params={"refresh_token": "bad-token"}
|
||||
"/api/token/refresh", json={"refresh_token": "bad-token"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
|
@ -109,6 +109,6 @@ async def test_refresh_token_access_token_as_refresh_returns_401(
|
|||
access_token = response.json()["access_token"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/token/refresh", params={"refresh_token": access_token}
|
||||
"/api/token/refresh", json={"refresh_token": access_token}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
|
|
|||
|
|
@ -20,9 +20,8 @@ async def create_entry_route(
|
|||
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)
|
||||
meal = await ctx.repo.meal.get_by_id_and_user(meal_id, ctx.user.id)
|
||||
entry = await create_entry(ctx, meal=meal, data=data)
|
||||
return entry
|
||||
|
||||
|
||||
|
|
@ -35,9 +34,7 @@ async def update_entry(
|
|||
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)
|
||||
entry = await ctx.repo.entry.get_by_id_and_user(entry_id, ctx.user.id)
|
||||
await EntryController(ctx, entry).update(data)
|
||||
return entry
|
||||
|
||||
|
|
@ -50,7 +47,5 @@ async def delete_entry(
|
|||
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)
|
||||
entry = await ctx.repo.entry.get_by_id_and_user(entry_id, ctx.user.id)
|
||||
await ctx.repo.entry.delete(entry)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import datetime
|
|||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from fooder.command.load_preset_as_meal import load_preset_as_meal
|
||||
from fooder.command.save_meal_as_preset import save_meal_as_preset
|
||||
from fooder.context import AuthContextDependency, Context
|
||||
from fooder.controller.meal import MealController
|
||||
from fooder.model.meal import MealCreateModel, MealModel, MealUpdateModel
|
||||
from fooder.model.preset import LoadPresetAsMealModel, PresetModel, SaveAsPresetModel
|
||||
|
||||
router = APIRouter(tags=["meal"])
|
||||
|
||||
|
|
@ -23,6 +26,19 @@ async def create_meal(
|
|||
return ctrl.obj
|
||||
|
||||
|
||||
@router.post("/from_preset", response_model=MealModel, status_code=201)
|
||||
async def load_preset_as_meal_view(
|
||||
date: datetime.date,
|
||||
data: LoadPresetAsMealModel,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
diary = await ctx.repo.diary.get_by_user_and_date(ctx.user.id, date)
|
||||
preset = await ctx.repo.preset.get_by_id_and_user(data.preset_id, ctx.user.id)
|
||||
meal = await load_preset_as_meal(ctx, preset, diary_id=diary.id, name=data.name)
|
||||
return meal
|
||||
|
||||
|
||||
@router.patch("/{meal_id}", response_model=MealModel)
|
||||
async def update_meal(
|
||||
date: datetime.date,
|
||||
|
|
@ -31,12 +47,24 @@ async def update_meal(
|
|||
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)
|
||||
meal = await ctx.repo.meal.get_by_id_and_user(meal_id, ctx.user.id)
|
||||
await MealController(ctx, meal).update(data)
|
||||
return meal
|
||||
|
||||
|
||||
@router.post("/{meal_id}/preset", response_model=PresetModel, status_code=201)
|
||||
async def save_meal_as_preset_view(
|
||||
date: datetime.date,
|
||||
meal_id: int,
|
||||
data: SaveAsPresetModel,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
meal = await ctx.repo.meal.get_by_id_and_user(meal_id, ctx.user.id)
|
||||
preset = await save_meal_as_preset(ctx, meal, name=data.name)
|
||||
return preset
|
||||
|
||||
|
||||
@router.delete("/{meal_id}", status_code=204)
|
||||
async def delete_meal(
|
||||
date: datetime.date,
|
||||
|
|
@ -44,6 +72,5 @@ async def delete_meal(
|
|||
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)
|
||||
meal = await ctx.repo.meal.get_by_id_and_user(meal_id, ctx.user.id)
|
||||
await ctx.repo.meal.delete(meal)
|
||||
|
|
|
|||
81
fooder/view/preset.py
Normal file
81
fooder/view/preset.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
|
||||
from fooder.context import AuthContextDependency, Context
|
||||
from fooder.controller.preset import PresetController
|
||||
from fooder.controller.preset_entry import PresetEntryController
|
||||
from fooder.model.entry import EntryCreateModel, EntryUpdateModel
|
||||
from fooder.model.preset import PresetModel, PresetUpdateModel
|
||||
from fooder.model.preset_entry import PresetEntryModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_auth_ctx = AuthContextDependency()
|
||||
|
||||
|
||||
@router.get("", response_model=list[PresetModel])
|
||||
async def list_presets(
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
):
|
||||
return await ctx.repo.preset.list_by_user(
|
||||
user_id=ctx.user.id, limit=limit, offset=offset
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{preset_id}", response_model=PresetModel)
|
||||
async def update_preset(
|
||||
preset_id: int,
|
||||
data: PresetUpdateModel,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
preset = await ctx.repo.preset.get_by_id_and_user(preset_id, ctx.user.id)
|
||||
await PresetController(ctx, preset).update(data)
|
||||
return preset
|
||||
|
||||
|
||||
@router.delete("/{preset_id}", status_code=204)
|
||||
async def delete_preset(
|
||||
preset_id: int,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
preset = await ctx.repo.preset.get_by_id_and_user(preset_id, ctx.user.id)
|
||||
await ctx.repo.preset.delete(preset)
|
||||
|
||||
|
||||
@router.post("/{preset_id}/entry", response_model=PresetEntryModel, status_code=201)
|
||||
async def create_preset_entry(
|
||||
preset_id: int,
|
||||
data: EntryCreateModel,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
preset = await ctx.repo.preset.get_by_id_and_user(preset_id, ctx.user.id)
|
||||
ctrl = await PresetEntryController.create(ctx, preset=preset, data=data)
|
||||
return ctrl.obj
|
||||
|
||||
|
||||
@router.patch("/{preset_id}/entry/{entry_id}", response_model=PresetEntryModel)
|
||||
async def update_preset_entry(
|
||||
preset_id: int,
|
||||
entry_id: int,
|
||||
data: EntryUpdateModel,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
entry = await ctx.repo.preset_entry.get_by_id_and_user(entry_id, ctx.user.id)
|
||||
await PresetEntryController(ctx, entry).update(data)
|
||||
return entry
|
||||
|
||||
|
||||
@router.delete("/{preset_id}/entry/{entry_id}", status_code=204)
|
||||
async def delete_preset_entry(
|
||||
preset_id: int,
|
||||
entry_id: int,
|
||||
ctx: Context = Depends(_auth_ctx),
|
||||
):
|
||||
async with ctx.repo.transaction():
|
||||
entry = await ctx.repo.preset_entry.get_by_id_and_user(entry_id, ctx.user.id)
|
||||
await ctx.repo.preset_entry.delete(entry)
|
||||
|
|
@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends
|
|||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from datetime import datetime
|
||||
|
||||
from fooder.model.token import TokenResponse
|
||||
from fooder.model.token import TokenResponse, RefreshTokenRequest
|
||||
from fooder.context import ContextDependency, Context
|
||||
from fooder.controller.user import UserController
|
||||
from fooder.utils.jwt import RefreshToken, generate_token_pair
|
||||
|
|
@ -34,9 +34,9 @@ async def token_create(
|
|||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def token_refresh(
|
||||
refresh_token: str,
|
||||
data: RefreshTokenRequest,
|
||||
ctx: Context = Depends(_ctx),
|
||||
) -> TokenResponse:
|
||||
now = ctx.clock()
|
||||
token = RefreshToken.decode(refresh_token)
|
||||
token = RefreshToken.decode(data.refresh_token)
|
||||
return gen_token_response(token.sub, now)
|
||||
|
|
|
|||
Loading…
Reference in a new issue