diff --git a/mealie/db/database.py b/mealie/db/database.py index 6a1b42143..eb22f6c3c 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -5,6 +5,7 @@ from mealie.db.db_base import BaseDocument from mealie.db.models.event import Event, EventNotification from mealie.db.models.group import Group from mealie.db.models.mealplan import MealPlan +from mealie.db.models.recipe.comment import RecipeComment from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag from mealie.db.models.settings import CustomPage, SiteSettings from mealie.db.models.shopping_list import ShoppingList @@ -12,6 +13,7 @@ from mealie.db.models.sign_up import SignUp from mealie.db.models.theme import SiteThemeModel from mealie.db.models.users import LongLiveToken, User from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse +from mealie.schema.comments import CommentOut from mealie.schema.event_notifications import EventNotificationIn from mealie.schema.events import Event as EventSchema from mealie.schema.meal import MealPlanOut @@ -110,6 +112,13 @@ class _Users(BaseDocument): return self.schema.from_orm(entry) +class _Comments(BaseDocument): + def __init__(self) -> None: + self.primary_key = "id" + self.sql_model = RecipeComment + self.schema = CommentOut + + class _LongLiveToken(BaseDocument): def __init__(self) -> None: self.primary_key = "id" @@ -190,6 +199,7 @@ class Database: self.events = _Events() self.event_notifications = _EventNotification() self.shopping_lists = _ShoppingList() + self.comments = _Comments() db = Database() diff --git a/mealie/db/models/model_base.py b/mealie/db/models/model_base.py index c240f43f3..d3b23d3b0 100644 --- a/mealie/db/models/model_base.py +++ b/mealie/db/models/model_base.py @@ -8,6 +8,7 @@ class BaseMixins: def update(self, *args, **kwarg): self.__init__(*args, **kwarg) + @classmethod def get_ref(cls_type, session: Session, match_value: str, match_attr: str = "id"): eff_ref = getattr(cls_type, match_attr) return session.query(cls_type).filter(eff_ref == match_value).one_or_none() diff --git a/mealie/db/models/recipe/assets.py b/mealie/db/models/recipe/assets.py index 7fb1dab0a..0a2400623 100644 --- a/mealie/db/models/recipe/assets.py +++ b/mealie/db/models/recipe/assets.py @@ -16,7 +16,6 @@ class RecipeAsset(SqlAlchemyBase): icon=None, file_name=None, ) -> None: - print("Asset Saved", name) self.name = name self.file_name = file_name self.icon = icon diff --git a/mealie/db/models/recipe/comment.py b/mealie/db/models/recipe/comment.py new file mode 100644 index 000000000..189b1dfc3 --- /dev/null +++ b/mealie/db/models/recipe/comment.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase +from mealie.db.models.recipe.recipe import RecipeModel +from mealie.db.models.users import User +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, orm + + +class RecipeComment(SqlAlchemyBase, BaseMixins): + __tablename__ = "recipe_comments" + id = Column(Integer, primary_key=True) + parent_id = Column(Integer, ForeignKey("recipes.id")) + recipe = orm.relationship("RecipeModel", back_populates="comments") + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + user = orm.relationship("User", back_populates="comments", single_parent=True, foreign_keys=[user_id]) + date_added = Column(DateTime, default=datetime.now) + text = Column(String) + + def __init__(self, recipe_slug, user, text, session, **_) -> None: + self.text = text + self.user = User.get_ref(session, user) + self.recipe = RecipeModel.get_ref(session, recipe_slug, "slug") + + def update(self, text, **_) -> None: + self.text = text diff --git a/mealie/db/models/recipe/recipe.py b/mealie/db/models/recipe/recipe.py index 2812b6249..45a045e28 100644 --- a/mealie/db/models/recipe/recipe.py +++ b/mealie/db/models/recipe/recipe.py @@ -55,6 +55,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins): collection_class=ordering_list("position"), ) + comments: list = orm.relationship("RecipeComment", back_populates="recipe", cascade="all, delete, delete-orphan") + # Mealie Specific slug = sa.Column(sa.String, index=True, unique=True) settings = orm.relationship("RecipeSettings", uselist=False, cascade="all, delete-orphan") diff --git a/mealie/db/models/users.py b/mealie/db/models/users.py index 43cf42a74..785f3e263 100644 --- a/mealie/db/models/users.py +++ b/mealie/db/models/users.py @@ -33,6 +33,10 @@ class User(SqlAlchemyBase, BaseMixins): LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True ) + comments: list = orm.relationship( + "RecipeComment", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True + ) + favorite_recipes: list[RecipeModel] = orm.relationship(RecipeModel, back_populates="favorited_by") def __init__( @@ -56,7 +60,7 @@ class User(SqlAlchemyBase, BaseMixins): self.password = password self.favorite_recipes = [ - RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug") + RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes ] @@ -78,7 +82,7 @@ class User(SqlAlchemyBase, BaseMixins): self.password = password self.favorite_recipes = [ - RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug") + RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes ] diff --git a/mealie/routes/recipe/__init__.py b/mealie/routes/recipe/__init__.py index fb9a0d55b..cd78cf574 100644 --- a/mealie/routes/recipe/__init__.py +++ b/mealie/routes/recipe/__init__.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes +from mealie.routes.recipe import all_recipe_routes, category_routes, comments, recipe_crud_routes, tag_routes recipe_router = APIRouter() @@ -7,3 +7,4 @@ recipe_router.include_router(all_recipe_routes.router) recipe_router.include_router(recipe_crud_routes.router) recipe_router.include_router(category_routes.router) recipe_router.include_router(tag_routes.router) +recipe_router.include_router(comments.router) diff --git a/mealie/routes/recipe/comments.py b/mealie/routes/recipe/comments.py new file mode 100644 index 000000000..2adbedf39 --- /dev/null +++ b/mealie/routes/recipe/comments.py @@ -0,0 +1,54 @@ +from http.client import HTTPException + +from fastapi import APIRouter, Depends, status +from mealie.db.database import db +from mealie.db.db_setup import generate_session +from mealie.routes.deps import get_current_user +from mealie.schema.comments import CommentIn, CommentOut, CommentSaveToDB +from mealie.schema.user import UserInDB +from sqlalchemy.orm.session import Session + +router = APIRouter(prefix="/api", tags=["Recipe Comments"]) + + +@router.post("/recipes/{slug}/comments") +async def create_comment( + slug: str, + new_comment: CommentIn, + session: Session = Depends(generate_session), + current_user: UserInDB = Depends(get_current_user), +): + """ Create comment in the Database """ + + new_comment = CommentSaveToDB(user=current_user.id, text=new_comment.text, recipe_slug=slug) + return db.comments.create(session, new_comment) + + +@router.put("/recipes/{slug}/comments/{id}") +async def update_comment( + id: int, + new_comment: CommentIn, + session: Session = Depends(generate_session), + current_user: UserInDB = Depends(get_current_user), +): + """ Update comment in the Database """ + old_comment: CommentOut = db.comments.get(session, id) + + if current_user.id != old_comment.user.id: + raise HTTPException(status.HTTP_401_UNAUTHORIZED) + + return db.comments.update(session, id, new_comment) + + +@router.delete("/recipes/{slug}/comments/{id}") +async def delete_comment( + id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user) +): + """ Delete comment from the Database """ + comment: CommentOut = db.comments.get(session, id) + print(current_user.id, comment.user.id, current_user.admin) + if current_user.id == comment.user.id or current_user.admin: + db.comments.delete(session, id) + return + + raise HTTPException(status.HTTP_401_UNAUTHORIZED) diff --git a/mealie/schema/comments.py b/mealie/schema/comments.py new file mode 100644 index 000000000..6da1c78b3 --- /dev/null +++ b/mealie/schema/comments.py @@ -0,0 +1,43 @@ +from datetime import datetime +from typing import Optional + +from fastapi_camelcase import CamelModel +from pydantic.utils import GetterDict + + +class UserBase(CamelModel): + id: int + username: Optional[str] + admin: bool + + class Config: + orm_mode = True + + +class CommentIn(CamelModel): + text: str + + +class CommentSaveToDB(CommentIn): + recipe_slug: str + user: int + + class Config: + orm_mode = True + + +class CommentOut(CommentIn): + id: int + recipe_slug: str + date_added: datetime + user: UserBase + + class Config: + orm_mode = True + + @classmethod + def getter_dict(_cls, name_orm): + return { + **GetterDict(name_orm), + "recipe_slug": name_orm.recipe.slug, + } diff --git a/mealie/schema/recipe.py b/mealie/schema/recipe.py index 521c6d888..bd11f9f29 100644 --- a/mealie/schema/recipe.py +++ b/mealie/schema/recipe.py @@ -5,6 +5,7 @@ from typing import Any, Optional from fastapi_camelcase import CamelModel from mealie.core.config import app_dirs from mealie.db.models.recipe.recipe import RecipeModel +from mealie.schema.comments import CommentOut from pydantic import BaseModel, Field, validator from pydantic.utils import GetterDict from slugify import slugify @@ -102,6 +103,8 @@ class Recipe(RecipeSummary): org_url: Optional[str] = Field(None, alias="orgURL") extras: Optional[dict] = {} + comments: Optional[list[CommentOut]] = [] + @staticmethod def directory_from_slug(slug) -> Path: return app_dirs.RECIPE_DATA_DIR.joinpath(slug)