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:
Hayden
2021-05-29 15:54:18 -08:00
committed by GitHub
parent 57f7ea3750
commit 6f38fcf81b
38 changed files with 365 additions and 82 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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()

View File

@@ -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__(**_)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,5 @@
from fastapi_camelcase import CamelModel
class RecipeSlug(CamelModel):
slug: str

View File

@@ -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