[user settings] endpionts

This commit is contained in:
Piotr Domański 2026-04-07 17:29:36 +02:00
parent 13fd45e72e
commit 0ea4c0b4b0
9 changed files with 176 additions and 1 deletions

View file

@ -0,0 +1,19 @@
from fooder.controller.base import ModelController
from fooder.domain import UserSettings
from fooder.model.user_settings import UserSettingsUpdateModel
class UserSettingsController(ModelController[UserSettings]):
async def update(self, data: UserSettingsUpdateModel) -> 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.user_settings.update(self.obj)

View file

@ -0,0 +1,18 @@
from .base import ObjModelMixin, Calories, OptionalCalories
from pydantic import BaseModel
class UserSettingsModel(ObjModelMixin, BaseModel):
protein_goal: Calories
carb_goal: Calories
fat_goal: Calories
fiber_goal: Calories
calories_goal: Calories
class UserSettingsUpdateModel(BaseModel):
protein_goal: OptionalCalories = None
carb_goal: OptionalCalories = None
fat_goal: OptionalCalories = None
fiber_goal: OptionalCalories = None
calories_goal: OptionalCalories = None

View file

@ -6,7 +6,8 @@ from sqlalchemy.exc import IntegrityError
from fooder.repository.user import UserRepository 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.domain import User, Product, UserProductUsage from fooder.repository.user_settings import UserSettingsRepository
from fooder.domain import User, Product, UserProductUsage, UserSettings
from fooder.exc import Conflict from fooder.exc import Conflict
@ -16,6 +17,7 @@ class Repository:
self.user = UserRepository(User, session) self.user = UserRepository(User, session)
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)
async def commit(self) -> None: async def commit(self) -> None:
try: try:

View file

@ -0,0 +1,7 @@
from fooder.domain import UserSettings
from .base import RepositoryBase
class UserSettingsRepository(RepositoryBase[UserSettings]):
async def get_by_user_id(self, user_id: int) -> UserSettings:
return await self._get(UserSettings.user_id == user_id)

View file

@ -2,7 +2,11 @@ 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
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"])
router.include_router(product_router, prefix="/product", tags=["product"]) router.include_router(product_router, prefix="/product", tags=["product"])
router.include_router(
user_settings_router, prefix="/user/settings", tags=["user_settings"]
)

View file

@ -5,6 +5,7 @@ from .user import *
from .client import * from .client import *
from .context import * from .context import *
from .product import * from .product import *
from .user_settings import *
@pytest.fixture @pytest.fixture

17
fooder/test/fixtures/user_settings.py vendored Normal file
View file

@ -0,0 +1,17 @@
import pytest_asyncio
from fooder.domain.user_settings import UserSettings
@pytest_asyncio.fixture
async def user_settings(ctx, user):
settings = UserSettings(
user_id=user.id,
protein_goal=150.0,
carb_goal=200.0,
fat_goal=70.0,
fiber_goal=30.0,
calories_goal=2000.0,
)
await ctx.repo.user_settings.create(settings)
return settings

View file

@ -0,0 +1,80 @@
async def test_get_user_settings_returns_200(auth_client, user_settings):
response = await auth_client.get("/api/user/settings")
assert response.status_code == 200
async def test_get_user_settings_returns_correct_fields(auth_client, user_settings):
response = await auth_client.get("/api/user/settings")
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
assert body["id"] == user_settings.id
async def test_get_user_settings_not_found_returns_404(auth_client):
response = await auth_client.get("/api/user/settings")
assert response.status_code == 404
async def test_get_user_settings_without_auth_returns_401(client):
response = await client.get("/api/user/settings")
assert response.status_code == 401
async def test_update_user_settings_returns_200(auth_client, user_settings):
response = await auth_client.patch(
"/api/user/settings", json={"protein_goal": 180.0}
)
assert response.status_code == 200
async def test_update_user_settings_partial_update(auth_client, user_settings):
response = await auth_client.patch(
"/api/user/settings", json={"protein_goal": 180.0}
)
body = response.json()
assert body["protein_goal"] == 180.0
assert body["carb_goal"] == user_settings.carb_goal
assert body["fat_goal"] == user_settings.fat_goal
assert body["calories_goal"] == user_settings.calories_goal
async def test_update_user_settings_all_fields(auth_client, user_settings):
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("/api/user/settings", 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_user_settings_not_found_returns_404(auth_client):
response = await auth_client.patch(
"/api/user/settings", json={"protein_goal": 180.0}
)
assert response.status_code == 404
async def test_update_user_settings_negative_value_returns_422(
auth_client, user_settings
):
response = await auth_client.patch(
"/api/user/settings", json={"protein_goal": -10.0}
)
assert response.status_code == 422
async def test_update_user_settings_without_auth_returns_401(client):
response = await client.patch("/api/user/settings", json={"protein_goal": 180.0})
assert response.status_code == 401

View file

@ -0,0 +1,27 @@
from fastapi import APIRouter, Depends
from fooder.context import Context, AuthContextDependency
from fooder.controller.user_settings import UserSettingsController
from fooder.model.user_settings import UserSettingsModel, UserSettingsUpdateModel
router = APIRouter(tags=["user_settings"])
_auth_ctx = AuthContextDependency()
@router.get("", response_model=UserSettingsModel)
async def get_user_settings(
ctx: Context = Depends(_auth_ctx),
):
return await ctx.repo.user_settings.get_by_user_id(ctx.user.id)
@router.patch("", response_model=UserSettingsModel)
async def update_user_settings(
data: UserSettingsUpdateModel,
ctx: Context = Depends(_auth_ctx),
):
async with ctx.repo.transaction():
settings = await ctx.repo.user_settings.get_by_user_id(ctx.user.id)
await UserSettingsController(ctx, settings).update(data)
return settings