mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-12-15 14:55:21 -05:00
feature/favorite-recipes (#443)
* add favorites options * bump dependencies * add badges to all cards * typo * remove console.log * fix site-loader viewport Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -78,7 +78,7 @@ class BaseDocument:
|
||||
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
||||
|
||||
def get(
|
||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False
|
||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||
) -> Union[BaseModel, list[BaseModel]]:
|
||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
||||
key is provided the class objects primary key will be used to match against.
|
||||
@@ -91,6 +91,7 @@ class BaseDocument:
|
||||
|
||||
Returns:
|
||||
dict or list[dict]:
|
||||
|
||||
"""
|
||||
if match_key is None:
|
||||
match_key = self.primary_key
|
||||
@@ -103,12 +104,14 @@ class BaseDocument:
|
||||
else:
|
||||
result = session.query(self.sql_model).filter_by(**{match_key: match_value}).limit(limit).all()
|
||||
|
||||
eff_schema = override_schema or self.schema
|
||||
|
||||
if limit == 1:
|
||||
try:
|
||||
return self.schema.from_orm(result[0])
|
||||
return eff_schema.from_orm(result[0])
|
||||
except IndexError:
|
||||
return None
|
||||
return [self.schema.from_orm(x) for x in result]
|
||||
return [eff_schema.from_orm(x) for x in result]
|
||||
|
||||
def create(self, session: Session, document: dict) -> BaseModel:
|
||||
"""Creates a new database entry for the given SQL Alchemy Model.
|
||||
|
||||
@@ -19,7 +19,7 @@ class EventNotification(SqlAlchemyBase, BaseMixins):
|
||||
user = Column(Boolean, default=False)
|
||||
|
||||
def __init__(
|
||||
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, *args, **kwargs
|
||||
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, **_
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.notification_url = notification_url
|
||||
@@ -41,7 +41,7 @@ class Event(SqlAlchemyBase, BaseMixins):
|
||||
time_stamp = Column(DateTime)
|
||||
category = Column(String)
|
||||
|
||||
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
|
||||
def __init__(self, title, text, time_stamp, category, **_) -> None:
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.time_stamp = time_stamp
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sqlalchemy.ext.declarative as dec
|
||||
from requests import Session
|
||||
|
||||
SqlAlchemyBase = dec.declarative_base()
|
||||
|
||||
@@ -6,3 +7,7 @@ SqlAlchemyBase = dec.declarative_base()
|
||||
class BaseMixins:
|
||||
def update(self, *args, **kwarg):
|
||||
self.__init__(*args, **kwarg)
|
||||
|
||||
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()
|
||||
|
||||
@@ -68,6 +68,10 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
date_added = sa.Column(sa.Date, default=date.today)
|
||||
date_updated = sa.Column(sa.DateTime)
|
||||
|
||||
# Favorited By
|
||||
favorited_by_id = sa.Column(sa.Integer, sa.ForeignKey("users.id"))
|
||||
favorited_by = orm.relationship("User", back_populates="favorite_recipes")
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, key, name):
|
||||
assert name != ""
|
||||
@@ -99,8 +103,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
extras: dict = None,
|
||||
assets: list = None,
|
||||
settings: dict = None,
|
||||
*args,
|
||||
**kwargs
|
||||
**_
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -138,6 +141,6 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
self.date_added = date_added
|
||||
self.date_updated = datetime.datetime.now()
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
def update(self, **_):
|
||||
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""
|
||||
self.__init__(*args, **kwargs)
|
||||
self.__init__(**_)
|
||||
|
||||
@@ -42,7 +42,7 @@ class CustomPage(SqlAlchemyBase, BaseMixins):
|
||||
slug = sa.Column(sa.String, nullable=False)
|
||||
categories = orm.relationship("Category", secondary=custom_pages2categories, single_parent=True)
|
||||
|
||||
def __init__(self, session=None, name=None, slug=None, position=0, categories=[], *args, **kwargs) -> None:
|
||||
def __init__(self, session=None, name=None, slug=None, position=0, categories=[], **_) -> None:
|
||||
self.name = name
|
||||
self.slug = slug
|
||||
self.position = position
|
||||
|
||||
@@ -9,7 +9,7 @@ class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
colors = orm.relationship("ThemeColorsModel", uselist=False, single_parent=True, cascade="all, delete-orphan")
|
||||
|
||||
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
||||
def __init__(self, name: str, colors: dict, **_) -> None:
|
||||
self.name = name
|
||||
self.colors = ThemeColorsModel(**colors)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from mealie.core.config import settings
|
||||
from mealie.db.models.group import Group
|
||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from mealie.db.models.recipe.recipe import RecipeModel
|
||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||
|
||||
|
||||
@@ -22,11 +23,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True)
|
||||
full_name = Column(String, index=True)
|
||||
username = Column(
|
||||
String,
|
||||
index=True,
|
||||
unique=True,
|
||||
)
|
||||
username = Column(String, index=True, unique=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
password = Column(String)
|
||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||
@@ -36,21 +33,38 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||
)
|
||||
|
||||
favorite_recipes: list[RecipeModel] = orm.relationship(RecipeModel, back_populates="favorited_by")
|
||||
|
||||
def __init__(
|
||||
self, session, full_name, email, password, group: str = settings.DEFAULT_GROUP, admin=False, **_
|
||||
self,
|
||||
session,
|
||||
full_name,
|
||||
email,
|
||||
password,
|
||||
favorite_recipes: list[str] = None,
|
||||
group: str = settings.DEFAULT_GROUP,
|
||||
admin=False,
|
||||
**_
|
||||
) -> None:
|
||||
|
||||
group = group or settings.DEFAULT_GROUP
|
||||
favorite_recipes = favorite_recipes or []
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.group = Group.get_ref(session, group)
|
||||
self.admin = admin
|
||||
self.password = password
|
||||
|
||||
self.favorite_recipes = [
|
||||
RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug")
|
||||
for x in favorite_recipes
|
||||
]
|
||||
|
||||
if self.username is None:
|
||||
self.username = full_name
|
||||
|
||||
def update(self, full_name, email, group, admin, username, session=None, id=None, password=None, *args, **kwargs):
|
||||
def update(self, full_name, email, group, admin, username, session=None, favorite_recipes=None, password=None, **_):
|
||||
favorite_recipes = favorite_recipes or []
|
||||
self.username = username
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
@@ -63,6 +77,11 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
self.favorite_recipes = [
|
||||
RecipeModel.get_ref(RecipeModel, session=session, match_value=x, match_attr="slug")
|
||||
for x in favorite_recipes
|
||||
]
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from mealie.core.security import get_password_hash, verify_password
|
||||
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.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from mealie.schema.user import ChangePassword, UserBase, UserFavorites, UserIn, UserInDB, UserOut
|
||||
from mealie.services.events import create_user_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
@@ -19,7 +19,7 @@ router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
async def create_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
new_user: UserIn,
|
||||
current_user=Depends(get_current_user),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
@@ -45,24 +45,21 @@ async def get_all_users(
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_logged_in_user(
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=UserOut)
|
||||
@router.get("/{id}", response_model=UserOut, dependencies=[Depends(get_current_user)])
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return db.users.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}/reset-password")
|
||||
@router.put("/{id}/reset-password", dependencies=[Depends(get_current_user)])
|
||||
async def reset_user_password(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
@@ -97,11 +94,10 @@ async def get_user_image(id: str):
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/{id}/image")
|
||||
@router.post("/{id}/image", dependencies=[Depends(get_current_user)])
|
||||
async def update_user_image(
|
||||
id: str,
|
||||
profile_image: UploadFile = File(...),
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
):
|
||||
""" Updates a User Image """
|
||||
|
||||
@@ -139,6 +135,41 @@ async def update_password(
|
||||
db.users.update_password(session, id, new_password)
|
||||
|
||||
|
||||
@router.get("/{id}/favorites", response_model=UserFavorites)
|
||||
async def get_favorites(id: str, session: Session = Depends(generate_session)):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
|
||||
return db.users.get(session, id, override_schema=UserFavorites)
|
||||
|
||||
|
||||
@router.post("/{id}/favorites/{slug}")
|
||||
async def add_favorite(
|
||||
slug: str,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
|
||||
current_user.favorite_recipes.append(slug)
|
||||
|
||||
db.users.update(session, current_user.id, current_user)
|
||||
|
||||
|
||||
@router.delete("/{id}/favorites/{slug}")
|
||||
async def remove_favorite(
|
||||
slug: str,
|
||||
current_user: UserInDB = Depends(get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Adds a Recipe to the users favorites """
|
||||
|
||||
current_user.favorite_recipes = [x for x in current_user.favorite_recipes if x != slug]
|
||||
|
||||
db.users.update(session, current_user.id, current_user)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
|
||||
5
mealie/schema/helpers.py
Normal file
5
mealie/schema/helpers.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class RecipeSlug(CamelModel):
|
||||
slug: str
|
||||
@@ -6,6 +6,7 @@ from mealie.db.models.group import Group
|
||||
from mealie.db.models.users import User
|
||||
from mealie.schema.category import CategoryBase
|
||||
from mealie.schema.meal import MealPlanOut
|
||||
from mealie.schema.recipe import RecipeSummary
|
||||
from mealie.schema.shopping_list import ShoppingListOut
|
||||
from pydantic.types import constr
|
||||
from pydantic.utils import GetterDict
|
||||
@@ -48,6 +49,7 @@ class UserBase(CamelModel):
|
||||
email: constr(to_lower=True, strip_whitespace=True)
|
||||
admin: bool
|
||||
group: Optional[str]
|
||||
favorite_recipes: Optional[list[str]] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -76,6 +78,22 @@ class UserOut(UserBase):
|
||||
id: int
|
||||
group: str
|
||||
tokens: Optional[list[LongLiveTokenOut]]
|
||||
favorite_recipes: Optional[list[str]] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@classmethod
|
||||
def getter_dict(cls, ormModel: User):
|
||||
return {
|
||||
**GetterDict(ormModel),
|
||||
"group": ormModel.group.name,
|
||||
"favorite_recipes": [x.slug for x in ormModel.favorite_recipes],
|
||||
}
|
||||
|
||||
|
||||
class UserFavorites(UserBase):
|
||||
favorite_recipes: list[RecipeSummary] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@@ -90,7 +108,6 @@ class UserOut(UserBase):
|
||||
|
||||
class UserInDB(UserOut):
|
||||
password: str
|
||||
pass
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
Reference in New Issue
Block a user