mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-14 14:25:53 -05:00
feat: User-specific Recipe Ratings (#3345)
This commit is contained in:
@@ -7,12 +7,14 @@ from pydantic import ConfigDict
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
from sqlalchemy.orm.attributes import get_history
|
||||
from sqlalchemy.orm.session import object_session
|
||||
|
||||
from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from ..users.user_to_favorite import users_to_favorites
|
||||
from ..users.user_to_recipe import UserToRecipe
|
||||
from .api_extras import ApiExtras, api_extras
|
||||
from .assets import RecipeAsset
|
||||
from .category import recipes_to_categories
|
||||
@@ -49,12 +51,20 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
user_id: Mapped[GUID | None] = mapped_column(GUID, sa.ForeignKey("users.id", use_alter=True), index=True)
|
||||
user: Mapped["User"] = orm.relationship("User", uselist=False, foreign_keys=[user_id])
|
||||
|
||||
meal_entries: Mapped[list["GroupMealPlan"]] = orm.relationship(
|
||||
"GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan"
|
||||
rating: Mapped[float | None] = mapped_column(sa.Float, index=True, nullable=True)
|
||||
rated_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User", secondary=UserToRecipe.__tablename__, back_populates="rated_recipes"
|
||||
)
|
||||
favorited_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
primaryjoin="and_(RecipeModel.id==UserToRecipe.recipe_id, UserToRecipe.is_favorite==True)",
|
||||
back_populates="favorite_recipes",
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
favorited_by: Mapped[list["User"]] = orm.relationship(
|
||||
"User", secondary=users_to_favorites, back_populates="favorite_recipes"
|
||||
meal_entries: Mapped[list["GroupMealPlan"]] = orm.relationship(
|
||||
"GroupMealPlan", back_populates="recipe", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# General Recipe Properties
|
||||
@@ -110,7 +120,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
)
|
||||
tags: Mapped[list["Tag"]] = orm.relationship("Tag", secondary=recipes_to_tags, back_populates="recipes")
|
||||
notes: Mapped[list[Note]] = orm.relationship("Note", cascade="all, delete-orphan")
|
||||
rating: Mapped[int | None] = mapped_column(sa.Integer)
|
||||
org_url: Mapped[str | None] = mapped_column(sa.String)
|
||||
extras: Mapped[list[ApiExtras]] = orm.relationship("ApiExtras", cascade="all, delete-orphan")
|
||||
is_ocr_recipe: Mapped[bool | None] = mapped_column(sa.Boolean, default=False)
|
||||
@@ -246,3 +255,23 @@ def receive_description(target: RecipeModel, value: str, oldvalue, initiator):
|
||||
target.description_normalized = RecipeModel.normalize(value)
|
||||
else:
|
||||
target.description_normalized = None
|
||||
|
||||
|
||||
@event.listens_for(RecipeModel, "before_update")
|
||||
def calculate_rating(mapper, connection, target: RecipeModel):
|
||||
session = object_session(target)
|
||||
if not session:
|
||||
return
|
||||
|
||||
if session.is_modified(target, "rating"):
|
||||
history = get_history(target, "rating")
|
||||
old_value = history.deleted[0] if history.deleted else None
|
||||
new_value = history.added[0] if history.added else None
|
||||
if old_value == new_value:
|
||||
return
|
||||
|
||||
target.rating = (
|
||||
session.query(sa.func.avg(UserToRecipe.rating))
|
||||
.filter(UserToRecipe.recipe_id == target.id, UserToRecipe.rating is not None, UserToRecipe.rating > 0)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .password_reset import *
|
||||
from .user_to_favorite import *
|
||||
from .user_to_recipe import *
|
||||
from .users import *
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint
|
||||
|
||||
from .._model_base import SqlAlchemyBase
|
||||
from .._model_utils import GUID
|
||||
|
||||
users_to_favorites = Table(
|
||||
"users_to_favorites",
|
||||
SqlAlchemyBase.metadata,
|
||||
Column("user_id", GUID, ForeignKey("users.id"), index=True),
|
||||
Column("recipe_id", GUID, ForeignKey("recipes.id"), index=True),
|
||||
UniqueConstraint("user_id", "recipe_id", name="user_id_recipe_id_key"),
|
||||
)
|
||||
42
mealie/db/models/users/user_to_recipe.py
Normal file
42
mealie/db/models/users/user_to_recipe.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from sqlalchemy import Boolean, Column, Float, ForeignKey, UniqueConstraint, event
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import GUID, auto_init
|
||||
|
||||
|
||||
class UserToRecipe(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users_to_recipes"
|
||||
__table_args__ = (UniqueConstraint("user_id", "recipe_id", name="user_id_recipe_id_rating_key"),)
|
||||
id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate)
|
||||
|
||||
user_id = Column(GUID, ForeignKey("users.id"), index=True, primary_key=True)
|
||||
recipe_id = Column(GUID, ForeignKey("recipes.id"), index=True, primary_key=True)
|
||||
rating = Column(Float, index=True, nullable=True)
|
||||
is_favorite = Column(Boolean, index=True, nullable=False)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def update_recipe_rating(session: Session, target: UserToRecipe):
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
|
||||
recipe = session.query(RecipeModel).filter(RecipeModel.id == target.recipe_id).first()
|
||||
if not recipe:
|
||||
return
|
||||
|
||||
recipe.rating = -1 # this will trigger the recipe to re-calculate the rating
|
||||
|
||||
|
||||
@event.listens_for(UserToRecipe, "after_insert")
|
||||
@event.listens_for(UserToRecipe, "after_update")
|
||||
@event.listens_for(UserToRecipe, "after_delete")
|
||||
def update_recipe_rating_on_insert_or_delete(_, connection: Connection, target: UserToRecipe):
|
||||
session = Session(bind=connection)
|
||||
|
||||
update_recipe_rating(session, target)
|
||||
session.commit()
|
||||
@@ -12,7 +12,7 @@ from mealie.db.models._model_utils.guid import GUID
|
||||
|
||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||
from .._model_utils import auto_init
|
||||
from .user_to_favorite import users_to_favorites
|
||||
from .user_to_recipe import UserToRecipe
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..group import Group
|
||||
@@ -49,7 +49,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
username: Mapped[str | None] = mapped_column(String, index=True, unique=True)
|
||||
email: Mapped[str | None] = mapped_column(String, unique=True, index=True)
|
||||
password: Mapped[str | None] = mapped_column(String)
|
||||
auth_method: Mapped[Enum(AuthMethod)] = mapped_column(Enum(AuthMethod), default=AuthMethod.MEALIE)
|
||||
auth_method: Mapped[Enum[AuthMethod]] = mapped_column(Enum(AuthMethod), default=AuthMethod.MEALIE)
|
||||
admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
advanced: Mapped[bool | None] = mapped_column(Boolean, default=False)
|
||||
|
||||
@@ -84,8 +84,15 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"GroupMealPlan", order_by="GroupMealPlan.date", **sp_args
|
||||
)
|
||||
shopping_lists: Mapped[Optional["ShoppingList"]] = orm.relationship("ShoppingList", **sp_args)
|
||||
rated_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=UserToRecipe.__tablename__, back_populates="rated_by"
|
||||
)
|
||||
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
|
||||
"RecipeModel",
|
||||
secondary=UserToRecipe.__tablename__,
|
||||
primaryjoin="and_(User.id==UserToRecipe.user_id, UserToRecipe.is_favorite==True)",
|
||||
back_populates="favorited_by",
|
||||
viewonly=True,
|
||||
)
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
@@ -112,7 +119,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
|
||||
self.group = Group.get_by_name(session, group)
|
||||
|
||||
self.favorite_recipes = []
|
||||
self.rated_recipes = []
|
||||
|
||||
self.password = password
|
||||
|
||||
|
||||
Reference in New Issue
Block a user