mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-27 20:55:12 -05:00
feat: User-specific Recipe Ratings (#3345)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import api_tokens, crud, favorites, forgot_password, images, registration
|
||||
from . import api_tokens, crud, forgot_password, images, ratings, registration
|
||||
|
||||
# Must be used because of the way FastAPI works with nested routes
|
||||
user_prefix = "/users"
|
||||
@@ -13,4 +13,4 @@ router.include_router(crud.admin_router)
|
||||
router.include_router(forgot_password.router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||
router.include_router(images.router, prefix=user_prefix, tags=["Users: Images"])
|
||||
router.include_router(api_tokens.router)
|
||||
router.include_router(favorites.router, prefix=user_prefix, tags=["Users: Favorites"])
|
||||
router.include_router(ratings.router, prefix=user_prefix, tags=["Users: Ratings"])
|
||||
|
||||
@@ -11,7 +11,14 @@ from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||
from mealie.schema.response.pagination import PaginationQuery
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||
from mealie.schema.user.user import GroupInDB, UserPagination, UserSummary, UserSummaryPagination
|
||||
from mealie.schema.user.user import (
|
||||
GroupInDB,
|
||||
UserPagination,
|
||||
UserRatings,
|
||||
UserRatingSummary,
|
||||
UserSummary,
|
||||
UserSummaryPagination,
|
||||
)
|
||||
|
||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
||||
@@ -74,6 +81,25 @@ class UserController(BaseUserController):
|
||||
def get_logged_in_user(self):
|
||||
return self.user
|
||||
|
||||
@user_router.get("/self/ratings", response_model=UserRatings[UserRatingSummary])
|
||||
def get_logged_in_user_ratings(self):
|
||||
return UserRatings(ratings=self.repos.user_ratings.get_by_user(self.user.id))
|
||||
|
||||
@user_router.get("/self/ratings/{recipe_id}", response_model=UserRatingSummary)
|
||||
def get_logged_in_user_rating_for_recipe(self, recipe_id: UUID4):
|
||||
user_rating = self.repos.user_ratings.get_by_user_and_recipe(self.user.id, recipe_id)
|
||||
if user_rating:
|
||||
return user_rating
|
||||
else:
|
||||
raise HTTPException(
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
ErrorResponse.respond("User has not rated this recipe"),
|
||||
)
|
||||
|
||||
@user_router.get("/self/favorites", response_model=UserRatings[UserRatingSummary])
|
||||
def get_logged_in_user_favorites(self):
|
||||
return UserRatings(ratings=self.repos.user_ratings.get_by_user(self.user.id, favorites_only=True))
|
||||
|
||||
@user_router.get("/self/group", response_model=GroupInDB)
|
||||
def get_logged_in_user_group(self):
|
||||
return self.group
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import UserFavorites
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@controller(router)
|
||||
class UserFavoritesController(BaseUserController):
|
||||
@router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
async def get_favorites(self, id: UUID4):
|
||||
"""Get user's favorite recipes"""
|
||||
return self.repos.users.get_one(id, override_schema=UserFavorites)
|
||||
|
||||
@router.post("/{id}/favorites/{slug}")
|
||||
def add_favorite(self, id: UUID4, slug: str):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
|
||||
if not self.user.favorite_recipes:
|
||||
self.user.favorite_recipes = []
|
||||
|
||||
self.user.favorite_recipes.append(slug)
|
||||
self.repos.users.update(self.user.id, self.user)
|
||||
|
||||
@router.delete("/{id}/favorites/{slug}")
|
||||
def remove_favorite(self, id: UUID4, slug: str):
|
||||
"""Adds a Recipe to the users favorites"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
|
||||
if not self.user.favorite_recipes:
|
||||
self.user.favorite_recipes = []
|
||||
|
||||
self.user.favorite_recipes = [x for x in self.user.favorite_recipes if x != slug]
|
||||
self.repos.users.update(self.user.id, self.user)
|
||||
return
|
||||
81
mealie/routes/users/ratings.py
Normal file
81
mealie/routes/users/ratings.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.response.responses import ErrorResponse
|
||||
from mealie.schema.user.user import UserRatingCreate, UserRatingOut, UserRatings, UserRatingUpdate
|
||||
|
||||
router = UserAPIRouter()
|
||||
|
||||
|
||||
@controller(router)
|
||||
class UserRatingsController(BaseUserController):
|
||||
def get_recipe_or_404(self, slug_or_id: str | UUID):
|
||||
"""Fetches a recipe by slug or id, or raises a 404 error if not found."""
|
||||
if isinstance(slug_or_id, str):
|
||||
try:
|
||||
slug_or_id = UUID(slug_or_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
recipes_repo = self.repos.recipes.by_group(self.group_id)
|
||||
if isinstance(slug_or_id, UUID):
|
||||
recipe = recipes_repo.get_one(slug_or_id, key="id")
|
||||
else:
|
||||
recipe = recipes_repo.get_one(slug_or_id, key="slug")
|
||||
|
||||
if not recipe:
|
||||
raise HTTPException(
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
detail=ErrorResponse.respond(message="Not found."),
|
||||
)
|
||||
|
||||
return recipe
|
||||
|
||||
@router.get("/{id}/ratings", response_model=UserRatings[UserRatingOut])
|
||||
async def get_ratings(self, id: UUID4):
|
||||
"""Get user's rated recipes"""
|
||||
return UserRatings(ratings=self.repos.user_ratings.get_by_user(id))
|
||||
|
||||
@router.get("/{id}/favorites", response_model=UserRatings[UserRatingOut])
|
||||
async def get_favorites(self, id: UUID4):
|
||||
"""Get user's favorited recipes"""
|
||||
return UserRatings(ratings=self.repos.user_ratings.get_by_user(id, favorites_only=True))
|
||||
|
||||
@router.post("/{id}/ratings/{slug}")
|
||||
def set_rating(self, id: UUID4, slug: str, data: UserRatingUpdate):
|
||||
"""Sets the user's rating for a recipe"""
|
||||
assert_user_change_allowed(id, self.user)
|
||||
|
||||
recipe = self.get_recipe_or_404(slug)
|
||||
user_rating = self.repos.user_ratings.get_by_user_and_recipe(id, recipe.id)
|
||||
if not user_rating:
|
||||
self.repos.user_ratings.create(
|
||||
UserRatingCreate(
|
||||
user_id=id,
|
||||
recipe_id=recipe.id,
|
||||
rating=data.rating,
|
||||
is_favorite=data.is_favorite or False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
if data.rating is not None:
|
||||
user_rating.rating = data.rating
|
||||
if data.is_favorite is not None:
|
||||
user_rating.is_favorite = data.is_favorite
|
||||
|
||||
self.repos.user_ratings.update(user_rating.id, user_rating)
|
||||
|
||||
@router.post("/{id}/favorites/{slug}")
|
||||
def add_favorite(self, id: UUID4, slug: str):
|
||||
"""Adds a recipe to the user's favorites"""
|
||||
self.set_rating(id, slug, data=UserRatingUpdate(is_favorite=True))
|
||||
|
||||
@router.delete("/{id}/favorites/{slug}")
|
||||
def remove_favorite(self, id: UUID4, slug: str):
|
||||
"""Removes a recipe from the user's favorites"""
|
||||
self.set_rating(id, slug, data=UserRatingUpdate(is_favorite=False))
|
||||
Reference in New Issue
Block a user