mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat(backend): ✨ add initial cookbook support
This commit is contained in:
parent
83ab858e46
commit
d24e95c091
@ -24,7 +24,7 @@ class ReadDeps:
|
|||||||
user=Depends(is_logged_in),
|
user=Depends(is_logged_in),
|
||||||
):
|
):
|
||||||
self.session: Session = session
|
self.session: Session = session
|
||||||
self.bg_tasks: BackgroundTasks = background_tasks
|
self.bg_task: BackgroundTasks = background_tasks
|
||||||
self.user: bool = user
|
self.user: bool = user
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Generic, TypeVar, Union
|
from typing import Callable, Generic, TypeVar, Union
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
@ -22,11 +24,8 @@ class BaseAccessModel(Generic[T, D]):
|
|||||||
|
|
||||||
def __init__(self, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
|
def __init__(self, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
|
||||||
self.primary_key = primary_key
|
self.primary_key = primary_key
|
||||||
|
|
||||||
self.sql_model = sql_model
|
self.sql_model = sql_model
|
||||||
|
|
||||||
self.schema = schema
|
self.schema = schema
|
||||||
|
|
||||||
self.observers: list = []
|
self.observers: list = []
|
||||||
|
|
||||||
def subscribe(self, func: Callable) -> None:
|
def subscribe(self, func: Callable) -> None:
|
||||||
@ -82,40 +81,52 @@ class BaseAccessModel(Generic[T, D]):
|
|||||||
return [x.get(self.primary_key) for x in results_as_dict]
|
return [x.get(self.primary_key) for x in results_as_dict]
|
||||||
|
|
||||||
def _query_one(self, session: Session, match_value: str, match_key: str = None) -> D:
|
def _query_one(self, session: Session, match_value: str, match_key: str = None) -> D:
|
||||||
"""Query the sql database for one item an return the sql alchemy model
|
"""
|
||||||
|
Query the sql database for one item an return the sql alchemy model
|
||||||
object. If no match key is provided the primary_key attribute will be used.
|
object. If no match key is provided the primary_key attribute will be used.
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Session): Database Session object
|
|
||||||
match_value (str): The value to use in the query
|
|
||||||
match_key (str, optional): the key/property to match against. Defaults to None.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Union[Session, SqlAlchemyBase]: Will return both the session and found model
|
|
||||||
"""
|
"""
|
||||||
if match_key is None:
|
if match_key is None:
|
||||||
match_key = self.primary_key
|
match_key = self.primary_key
|
||||||
|
|
||||||
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
|
||||||
|
|
||||||
|
def get_one(self, session: Session, value: str | int, key: str = None, any_case=False, override_schema=None) -> T:
|
||||||
|
key = key or self.primary_key
|
||||||
|
|
||||||
|
if any_case:
|
||||||
|
search_attr = getattr(self.sql_model, key)
|
||||||
|
result = session.query(self.sql_model).filter(func.lower(search_attr) == key.lower()).one_or_none()
|
||||||
|
|
||||||
|
result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
eff_schema = override_schema or self.schema
|
||||||
|
return eff_schema.from_orm(result)
|
||||||
|
|
||||||
|
def get_many(
|
||||||
|
self, session: Session, value: str, key: str = None, limit=1, any_case=False, override_schema=None
|
||||||
|
) -> list[T]:
|
||||||
|
pass
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
|
||||||
) -> Union[T, list[T]]:
|
) -> T | list[T]:
|
||||||
"""Retrieves an entry from the database by matching a key/value pair. If no
|
"""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.
|
key is provided the class objects primary key will be used to match against.
|
||||||
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
match_value (str): A value used to match against the key/value in the database \n
|
match_value (str): A value used to match against the key/value in the database
|
||||||
match_key (str, optional): They key to match the value against. Defaults to None. \n
|
match_key (str, optional): They key to match the value against. Defaults to None.
|
||||||
limit (int, optional): A limit to returned responses. Defaults to 1. \n
|
limit (int, optional): A limit to returned responses. Defaults to 1.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict or list[dict]:
|
dict or list[dict]:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if match_key is None:
|
match_key = match_key or self.primary_key
|
||||||
match_key = self.primary_key
|
|
||||||
|
|
||||||
if any_case:
|
if any_case:
|
||||||
search_attr = getattr(self.sql_model, match_key)
|
search_attr = getattr(self.sql_model, match_key)
|
||||||
|
@ -3,6 +3,7 @@ from logging import getLogger
|
|||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
|
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
|
||||||
|
from mealie.db.models.cookbook import CookBook
|
||||||
from mealie.db.models.event import Event, EventNotification
|
from mealie.db.models.event import Event, EventNotification
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.mealplan import MealPlan
|
from mealie.db.models.mealplan import MealPlan
|
||||||
@ -10,12 +11,12 @@ from mealie.db.models.recipe.category import Category
|
|||||||
from mealie.db.models.recipe.comment import RecipeComment
|
from mealie.db.models.recipe.comment import RecipeComment
|
||||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel, Tag
|
from mealie.db.models.recipe.recipe import RecipeModel, Tag
|
||||||
from mealie.db.models.settings import CustomPage, SiteSettings
|
from mealie.db.models.settings import SiteSettings
|
||||||
from mealie.db.models.shopping_list import ShoppingList
|
from mealie.db.models.shopping_list import ShoppingList
|
||||||
from mealie.db.models.sign_up import SignUp
|
from mealie.db.models.sign_up import SignUp
|
||||||
from mealie.db.models.users import LongLiveToken, User
|
from mealie.db.models.users import LongLiveToken, User
|
||||||
from mealie.schema.admin import CustomPageOut
|
|
||||||
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
||||||
|
from mealie.schema.cookbook import ReadCookBook
|
||||||
from mealie.schema.events import Event as EventSchema
|
from mealie.schema.events import Event as EventSchema
|
||||||
from mealie.schema.events import EventNotificationIn
|
from mealie.schema.events import EventNotificationIn
|
||||||
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
|
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
|
||||||
@ -73,7 +74,6 @@ class DatabaseAccessLayer:
|
|||||||
# Site
|
# Site
|
||||||
self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
|
self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
|
||||||
self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
|
self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
|
||||||
self.custom_pages = BaseAccessModel(DEFAULT_PK, CustomPage, CustomPageOut)
|
|
||||||
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
|
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
|
||||||
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
|
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
|
||||||
|
|
||||||
@ -83,3 +83,4 @@ class DatabaseAccessLayer:
|
|||||||
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
|
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
|
||||||
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
|
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
|
||||||
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
|
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
|
||||||
|
self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)
|
||||||
|
@ -4,7 +4,7 @@ from typing import Union
|
|||||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
|
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
|
||||||
|
|
||||||
|
|
||||||
def handle_one_to_many_list(relation_cls, all_elements: list[dict]):
|
def handle_one_to_many_list(get_attr, relation_cls, all_elements: list[dict]):
|
||||||
elems_to_create = []
|
elems_to_create = []
|
||||||
updated_elems = []
|
updated_elems = []
|
||||||
|
|
||||||
@ -75,8 +75,17 @@ def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
|||||||
relation_cls = relationships[key].mapper.entity
|
relation_cls = relationships[key].mapper.entity
|
||||||
use_list = relationships[key].uselist
|
use_list = relationships[key].uselist
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_attr = relation_cls.Config.get_attr
|
||||||
|
if get_attr is None:
|
||||||
|
get_attr = "id"
|
||||||
|
except Exception:
|
||||||
|
get_attr = "id"
|
||||||
|
|
||||||
|
print(get_attr)
|
||||||
|
|
||||||
if relation_dir == ONETOMANY.name and use_list:
|
if relation_dir == ONETOMANY.name and use_list:
|
||||||
instances = handle_one_to_many_list(relation_cls, val)
|
instances = handle_one_to_many_list(get_attr, relation_cls, val)
|
||||||
setattr(self, key, instances)
|
setattr(self, key, instances)
|
||||||
|
|
||||||
if relation_dir == ONETOMANY.name and not use_list:
|
if relation_dir == ONETOMANY.name and not use_list:
|
||||||
@ -85,7 +94,7 @@ def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
|||||||
|
|
||||||
elif relation_dir == MANYTOONE.name and not use_list:
|
elif relation_dir == MANYTOONE.name and not use_list:
|
||||||
if isinstance(val, dict):
|
if isinstance(val, dict):
|
||||||
val = val.get("id")
|
val = val.get(get_attr)
|
||||||
|
|
||||||
if val is None:
|
if val is None:
|
||||||
raise ValueError(f"Expected 'id' to be provided for {key}")
|
raise ValueError(f"Expected 'id' to be provided for {key}")
|
||||||
@ -100,7 +109,7 @@ def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
|||||||
raise ValueError(f"Expected many to many input to be of type list for {key}")
|
raise ValueError(f"Expected many to many input to be of type list for {key}")
|
||||||
|
|
||||||
if len(val) > 0 and isinstance(val[0], dict):
|
if len(val) > 0 and isinstance(val[0], dict):
|
||||||
val = [elem.get("id") for elem in val]
|
val = [elem.get(get_attr) for elem in val]
|
||||||
|
|
||||||
instances = [x for x in [relation_cls.get_ref(elem, session=session) for elem in val] if x]
|
instances = [x for x in [relation_cls.get_ref(elem, session=session) for elem in val] if x]
|
||||||
setattr(self, key, instances)
|
setattr(self, key, instances)
|
||||||
|
24
mealie/db/models/cookbook.py
Normal file
24
mealie/db/models/cookbook.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
|
from ._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from ._model_utils import auto_init
|
||||||
|
from .recipe.category import Category, cookbooks_to_categories
|
||||||
|
|
||||||
|
|
||||||
|
class CookBook(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "cookbooks"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
position = Column(Integer, nullable=False)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
slug = Column(String, nullable=False)
|
||||||
|
categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
|
||||||
|
|
||||||
|
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||||
|
group = orm.relationship("Group", back_populates="cookbooks")
|
||||||
|
|
||||||
|
@auto_init()
|
||||||
|
def __init__(self, **_) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self, *args, **kwarg):
|
||||||
|
self.__init__(*args, **kwarg)
|
@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session
|
|||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import settings
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from mealie.db.models.cookbook import CookBook
|
||||||
from mealie.db.models.recipe.category import Category, group2categories
|
from mealie.db.models.recipe.category import Category, group2categories
|
||||||
|
|
||||||
|
|
||||||
@ -19,19 +20,9 @@ class Group(SqlAlchemyBase, BaseMixins):
|
|||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
|
||||||
users = orm.relationship("User", back_populates="group")
|
users = orm.relationship("User", back_populates="group")
|
||||||
mealplans = orm.relationship(
|
mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
|
||||||
"MealPlan",
|
shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
|
||||||
back_populates="group",
|
cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
|
||||||
single_parent=True,
|
|
||||||
order_by="MealPlan.start_date",
|
|
||||||
)
|
|
||||||
|
|
||||||
shopping_lists = orm.relationship(
|
|
||||||
"ShoppingList",
|
|
||||||
back_populates="group",
|
|
||||||
single_parent=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
|
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
|
||||||
|
|
||||||
# Webhook Settings
|
# Webhook Settings
|
||||||
|
@ -29,10 +29,10 @@ recipes2categories = sa.Table(
|
|||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
custom_pages2categories = sa.Table(
|
cookbooks_to_categories = sa.Table(
|
||||||
"custom_pages2categories",
|
"cookbooks_to_categories",
|
||||||
SqlAlchemyBase.metadata,
|
SqlAlchemyBase.metadata,
|
||||||
sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")),
|
sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
|
||||||
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,6 +61,7 @@ class Category(SqlAlchemyBase, BaseMixins):
|
|||||||
if not session or not match_value:
|
if not session or not match_value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
print(match_value)
|
||||||
slug = slugify(match_value)
|
slug = slugify(match_value)
|
||||||
|
|
||||||
result = session.query(Category).filter(Category.slug == slug).one_or_none()
|
result = session.query(Category).filter(Category.slug == slug).one_or_none()
|
||||||
|
@ -3,7 +3,7 @@ import sqlalchemy.orm as orm
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.recipe.category import Category, custom_pages2categories, site_settings2categories
|
from mealie.db.models.recipe.category import Category, site_settings2categories
|
||||||
|
|
||||||
|
|
||||||
class SiteSettings(SqlAlchemyBase, BaseMixins):
|
class SiteSettings(SqlAlchemyBase, BaseMixins):
|
||||||
@ -33,21 +33,3 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
|
|||||||
|
|
||||||
def update(self, *args, **kwarg):
|
def update(self, *args, **kwarg):
|
||||||
self.__init__(*args, **kwarg)
|
self.__init__(*args, **kwarg)
|
||||||
|
|
||||||
|
|
||||||
class CustomPage(SqlAlchemyBase, BaseMixins):
|
|
||||||
__tablename__ = "custom_pages"
|
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
|
||||||
position = sa.Column(sa.Integer, nullable=False)
|
|
||||||
name = sa.Column(sa.String, nullable=False)
|
|
||||||
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=[], **_) -> None:
|
|
||||||
self.name = name
|
|
||||||
self.slug = slug
|
|
||||||
self.position = position
|
|
||||||
self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
|
|
||||||
|
|
||||||
def update(self, *args, **kwarg):
|
|
||||||
self.__init__(*args, **kwarg)
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import crud
|
from . import cookbooks, crud
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(cookbooks.user_router)
|
||||||
router.include_router(crud.user_router)
|
router.include_router(crud.user_router)
|
||||||
router.include_router(crud.admin_router)
|
router.include_router(crud.admin_router)
|
||||||
|
49
mealie/routes/groups/cookbooks.py
Normal file
49
mealie/routes/groups/cookbooks.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from fastapi import Depends
|
||||||
|
|
||||||
|
from mealie.routes.routers import UserAPIRouter
|
||||||
|
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook
|
||||||
|
from mealie.services.cookbook import CookbookService
|
||||||
|
|
||||||
|
user_router = UserAPIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.get("", response_model=list[ReadCookBook])
|
||||||
|
def get_all_cookbook(cb_service: CookbookService = Depends(CookbookService.private)):
|
||||||
|
""" Get cookbook from the Database """
|
||||||
|
# Get Item
|
||||||
|
return cb_service.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("", response_model=ReadCookBook)
|
||||||
|
def create_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.private)):
|
||||||
|
""" Create cookbook in the Database """
|
||||||
|
# Create Item
|
||||||
|
return cb_service.create_one(data)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.put("", response_model=list[ReadCookBook])
|
||||||
|
def update_many(data: list[ReadCookBook], cb_service: CookbookService = Depends(CookbookService.private)):
|
||||||
|
""" Create cookbook in the Database """
|
||||||
|
# Create Item
|
||||||
|
return cb_service.update_many(data)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.get("/{id}", response_model=ReadCookBook)
|
||||||
|
def get_cookbook(cb_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||||
|
""" Get cookbook from the Database """
|
||||||
|
# Get Item
|
||||||
|
return cb_service.cookbook
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.put("/{id}")
|
||||||
|
def update_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||||
|
""" Update cookbook in the Database """
|
||||||
|
# Update Item
|
||||||
|
return cb_service.update_one(data)
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.delete("/{id}")
|
||||||
|
def delete_cookbook(cd_service: CookbookService = Depends(CookbookService.write_existing)):
|
||||||
|
""" Delete cookbook from the Database """
|
||||||
|
# Delete Item
|
||||||
|
return cd_service.delete_one()
|
@ -31,13 +31,13 @@ def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existi
|
|||||||
|
|
||||||
|
|
||||||
@user_router.post("", status_code=201, response_model=str)
|
@user_router.post("", status_code=201, response_model=str)
|
||||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.base)) -> str:
|
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.private)) -> str:
|
||||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||||
return recipe_service.create_recipe(data).slug
|
return recipe_service.create_recipe(data).slug
|
||||||
|
|
||||||
|
|
||||||
@user_router.post("/create-url", status_code=201, response_model=str)
|
@user_router.post("/create-url", status_code=201, response_model=str)
|
||||||
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.base)):
|
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.private)):
|
||||||
""" Takes in a URL and attempts to scrape data and load it into the database """
|
""" Takes in a URL and attempts to scrape data and load it into the database """
|
||||||
|
|
||||||
recipe = create_from_url(url.url)
|
recipe = create_from_url(url.url)
|
||||||
|
1
mealie/schema/cookbook/__init__.py
Normal file
1
mealie/schema/cookbook/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .cookbook import *
|
38
mealie/schema/cookbook/cookbook.py
Normal file
38
mealie/schema/cookbook/cookbook.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
from pydantic import validator
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
|
from ..recipe.recipe_category import CategoryBase
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCookBook(CamelModel):
|
||||||
|
name: str
|
||||||
|
slug: str = None
|
||||||
|
position: int = 1
|
||||||
|
categories: list[CategoryBase] = []
|
||||||
|
|
||||||
|
@validator("slug", always=True, pre=True)
|
||||||
|
def validate_slug(slug: str, values):
|
||||||
|
name: str = values["name"]
|
||||||
|
calc_slug: str = slugify(name)
|
||||||
|
|
||||||
|
if slug != calc_slug:
|
||||||
|
slug = calc_slug
|
||||||
|
|
||||||
|
return slug
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCookBook(CreateCookBook):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class SaveCookBook(CreateCookBook):
|
||||||
|
group_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ReadCookBook(UpdateCookBook):
|
||||||
|
group_id: int
|
||||||
|
categories: list[CategoryBase] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
@ -19,7 +19,6 @@ class CategoryBase(CategoryIn):
|
|||||||
def getter_dict(_cls, name_orm):
|
def getter_dict(_cls, name_orm):
|
||||||
return {
|
return {
|
||||||
**GetterDict(name_orm),
|
**GetterDict(name_orm),
|
||||||
"total_recipes": len(name_orm.recipes),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
from .category import *
|
|
||||||
from .comments import *
|
|
||||||
from .helpers import *
|
|
||||||
from .recipe import *
|
|
@ -1,48 +0,0 @@
|
|||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
|
||||||
from pydantic.utils import GetterDict
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryIn(CamelModel):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryBase(CategoryIn):
|
|
||||||
id: int
|
|
||||||
slug: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def getter_dict(_cls, name_orm):
|
|
||||||
return {
|
|
||||||
**GetterDict(name_orm),
|
|
||||||
"total_recipes": len(name_orm.recipes),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeCategoryResponse(CategoryBase):
|
|
||||||
recipes: Optional[List["Recipe"]]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
|
|
||||||
|
|
||||||
|
|
||||||
class TagIn(CategoryIn):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TagBase(CategoryBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeTagResponse(RecipeCategoryResponse):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from .recipe import Recipe
|
|
||||||
|
|
||||||
RecipeCategoryResponse.update_forward_refs()
|
|
||||||
RecipeTagResponse.update_forward_refs()
|
|
@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
uuid: str
|
|
||||||
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,
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
from fastapi_camelcase import CamelModel
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeSlug(CamelModel):
|
|
||||||
slug: str
|
|
@ -1,243 +0,0 @@
|
|||||||
import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from fastapi_camelcase import CamelModel
|
|
||||||
from pydantic import BaseModel, Field, validator
|
|
||||||
from pydantic.utils import GetterDict
|
|
||||||
from slugify import slugify
|
|
||||||
|
|
||||||
from mealie.core.config import app_dirs, settings
|
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel
|
|
||||||
|
|
||||||
from .comments import CommentOut
|
|
||||||
from .units_and_foods import IngredientFood, IngredientUnit
|
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipe(CamelModel):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeImageTypes(str, Enum):
|
|
||||||
original = "original.webp"
|
|
||||||
min = "min-original.webp"
|
|
||||||
tiny = "tiny-original.webp"
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeSettings(CamelModel):
|
|
||||||
public: bool = settings.RECIPE_PUBLIC
|
|
||||||
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
|
|
||||||
show_assets: bool = settings.RECIPE_SHOW_ASSETS
|
|
||||||
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
|
|
||||||
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
|
|
||||||
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeNote(BaseModel):
|
|
||||||
title: str
|
|
||||||
text: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeStep(CamelModel):
|
|
||||||
title: Optional[str] = ""
|
|
||||||
text: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeAsset(CamelModel):
|
|
||||||
name: str
|
|
||||||
icon: str
|
|
||||||
file_name: Optional[str]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class Nutrition(CamelModel):
|
|
||||||
calories: Optional[str]
|
|
||||||
fat_content: Optional[str]
|
|
||||||
protein_content: Optional[str]
|
|
||||||
carbohydrate_content: Optional[str]
|
|
||||||
fiber_content: Optional[str]
|
|
||||||
sodium_content: Optional[str]
|
|
||||||
sugar_content: Optional[str]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(CamelModel):
|
|
||||||
title: Optional[str]
|
|
||||||
note: Optional[str]
|
|
||||||
unit: Optional[IngredientUnit]
|
|
||||||
food: Optional[IngredientFood]
|
|
||||||
disable_amount: bool = True
|
|
||||||
quantity: int = 1
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeSummary(CamelModel):
|
|
||||||
id: Optional[int]
|
|
||||||
name: Optional[str]
|
|
||||||
slug: str = ""
|
|
||||||
image: Optional[Any]
|
|
||||||
|
|
||||||
description: Optional[str]
|
|
||||||
recipe_category: Optional[list[str]] = []
|
|
||||||
tags: Optional[list[str]] = []
|
|
||||||
rating: Optional[int]
|
|
||||||
|
|
||||||
date_added: Optional[datetime.date]
|
|
||||||
date_updated: Optional[datetime.datetime]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def getter_dict(_cls, name_orm: RecipeModel):
|
|
||||||
return {
|
|
||||||
**GetterDict(name_orm),
|
|
||||||
"recipe_category": [x.name for x in name_orm.recipe_category],
|
|
||||||
"tags": [x.name for x in name_orm.tags],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Recipe(RecipeSummary):
|
|
||||||
recipe_yield: Optional[str]
|
|
||||||
recipe_ingredient: Optional[list[RecipeIngredient]] = []
|
|
||||||
recipe_instructions: Optional[list[RecipeStep]] = []
|
|
||||||
nutrition: Optional[Nutrition]
|
|
||||||
tools: Optional[list[str]] = []
|
|
||||||
|
|
||||||
total_time: Optional[str] = None
|
|
||||||
prep_time: Optional[str] = None
|
|
||||||
perform_time: Optional[str] = None
|
|
||||||
|
|
||||||
# Mealie Specific
|
|
||||||
settings: Optional[RecipeSettings] = RecipeSettings()
|
|
||||||
assets: Optional[list[RecipeAsset]] = []
|
|
||||||
notes: Optional[list[RecipeNote]] = []
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def directory(self) -> Path:
|
|
||||||
dir = app_dirs.RECIPE_DATA_DIR.joinpath(self.slug)
|
|
||||||
dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
return dir
|
|
||||||
|
|
||||||
@property
|
|
||||||
def asset_dir(self) -> Path:
|
|
||||||
dir = self.directory.joinpath("assets")
|
|
||||||
dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
return dir
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image_dir(self) -> Path:
|
|
||||||
dir = self.directory.joinpath("images")
|
|
||||||
dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
return dir
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def getter_dict(_cls, name_orm: RecipeModel):
|
|
||||||
return {
|
|
||||||
**GetterDict(name_orm),
|
|
||||||
# "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
|
|
||||||
"recipe_category": [x.name for x in name_orm.recipe_category],
|
|
||||||
"tags": [x.name for x in name_orm.tags],
|
|
||||||
"tools": [x.tool for x in name_orm.tools],
|
|
||||||
"extras": {x.key_name: x.value for x in name_orm.extras},
|
|
||||||
}
|
|
||||||
|
|
||||||
schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"name": "Chicken and Rice With Leeks and Salsa Verde",
|
|
||||||
"description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
|
|
||||||
"image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
|
|
||||||
"recipe_yield": "4 Servings",
|
|
||||||
"recipe_ingredient": [
|
|
||||||
"1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
|
|
||||||
"Kosher salt, freshly ground pepper",
|
|
||||||
"3 Tbsp. unsalted butter, divided",
|
|
||||||
],
|
|
||||||
"recipe_instructions": [
|
|
||||||
{
|
|
||||||
"text": "Season chicken with salt and pepper.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"slug": "chicken-and-rice-with-leeks-and-salsa-verde",
|
|
||||||
"tags": ["favorite", "yummy!"],
|
|
||||||
"recipe_category": ["Dinner", "Pasta"],
|
|
||||||
"notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
|
|
||||||
"org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
|
|
||||||
"rating": 3,
|
|
||||||
"extras": {"message": "Don't forget to defrost the chicken!"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@validator("slug", always=True, pre=True)
|
|
||||||
def validate_slug(slug: str, values):
|
|
||||||
if not values["name"]:
|
|
||||||
return slug
|
|
||||||
name: str = values["name"]
|
|
||||||
calc_slug: str = slugify(name)
|
|
||||||
|
|
||||||
if slug != calc_slug:
|
|
||||||
slug = calc_slug
|
|
||||||
|
|
||||||
return slug
|
|
||||||
|
|
||||||
@validator("recipe_ingredient", always=True, pre=True)
|
|
||||||
def validate_ingredients(recipe_ingredient, values):
|
|
||||||
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
|
|
||||||
return recipe_ingredient
|
|
||||||
|
|
||||||
if all(isinstance(elem, str) for elem in recipe_ingredient):
|
|
||||||
return [RecipeIngredient(note=x) for x in recipe_ingredient]
|
|
||||||
|
|
||||||
return recipe_ingredient
|
|
||||||
|
|
||||||
|
|
||||||
class AllRecipeRequest(BaseModel):
|
|
||||||
properties: list[str]
|
|
||||||
limit: Optional[int]
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {
|
|
||||||
"example": {
|
|
||||||
"properties": ["name", "slug", "image"],
|
|
||||||
"limit": 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RecipeURLIn(BaseModel):
|
|
||||||
url: str
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
|
|
||||||
|
|
||||||
|
|
||||||
class SlugResponse(BaseModel):
|
|
||||||
class Config:
|
|
||||||
schema_extra = {"example": "adult-mac-and-cheese"}
|
|
@ -1,24 +0,0 @@
|
|||||||
from fastapi_camelcase import CamelModel
|
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientFood(CamelModel):
|
|
||||||
name: str
|
|
||||||
description: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientUnit(CreateIngredientFood):
|
|
||||||
abbreviation: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientFood(CreateIngredientFood):
|
|
||||||
id: int
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
||||||
|
|
||||||
|
|
||||||
class IngredientUnit(CreateIngredientUnit):
|
|
||||||
id: int
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
orm_mode = True
|
|
@ -48,25 +48,19 @@ class BaseHttpService(Generic[T, D]):
|
|||||||
def assert_existing(self, data: T) -> None:
|
def assert_existing(self, data: T) -> None:
|
||||||
raise NotImplementedError("`assert_existing` must by implemented by child class")
|
raise NotImplementedError("`assert_existing` must by implemented by child class")
|
||||||
|
|
||||||
|
def _create_event(self, title: str, message: str) -> None:
|
||||||
|
if not self.__class__.event_func:
|
||||||
|
raise NotImplementedError("`event_func` must be set by child class")
|
||||||
|
|
||||||
|
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_existing(cls, id: T, deps: ReadDeps = Depends()):
|
def read_existing(cls, id: T, deps: ReadDeps = Depends()):
|
||||||
"""
|
"""
|
||||||
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
||||||
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
||||||
|
|
||||||
Args:
|
|
||||||
slug (str): Recipe Slug used to query the database
|
|
||||||
session (Session, optional): The Injected SQLAlchemy Session.
|
|
||||||
user (bool, optional): The injected determination of is_logged_in.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 404 Not Found
|
|
||||||
HTTPException: 403 Forbidden
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
|
||||||
"""
|
"""
|
||||||
new_class = cls(deps.session, deps.user, deps.bg_tasks)
|
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||||
new_class.assert_existing(id)
|
new_class.assert_existing(id)
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
@ -75,35 +69,21 @@ class BaseHttpService(Generic[T, D]):
|
|||||||
"""
|
"""
|
||||||
Used for dependency injection for routes that require an existing recipe. The only difference between
|
Used for dependency injection for routes that require an existing recipe. The only difference between
|
||||||
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
||||||
|
|
||||||
Args:
|
|
||||||
slug (str): Recipe Slug used to query the database
|
|
||||||
session (Session, optional): The Injected SQLAlchemy Session.
|
|
||||||
user (bool, optional): The injected determination of is_logged_in.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: 404 Not Found
|
|
||||||
HTTPException: 403 Forbidden
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
|
||||||
"""
|
"""
|
||||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||||
new_class.assert_existing(id)
|
new_class.assert_existing(id)
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def base(cls, deps: WriteDeps = Depends()):
|
def public(cls, deps: ReadDeps = Depends()):
|
||||||
"""A Base instance to be used as a router dependency
|
"""
|
||||||
|
A Base instance to be used as a router dependency
|
||||||
Raises:
|
|
||||||
HTTPException: 400 Bad Request
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return cls(deps.session, deps.user, deps.bg_task)
|
return cls(deps.session, deps.user, deps.bg_task)
|
||||||
|
|
||||||
def _create_event(self, title: str, message: str) -> None:
|
@classmethod
|
||||||
if not self.__class__.event_func:
|
def private(cls, deps: WriteDeps = Depends()):
|
||||||
raise NotImplementedError("`event_func` must be set by child class")
|
"""
|
||||||
|
A Base instance to be used as a router dependency
|
||||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
"""
|
||||||
|
return cls(deps.session, deps.user, deps.bg_task)
|
||||||
|
1
mealie/services/cookbook/__init__.py
Normal file
1
mealie/services/cookbook/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .cookbook_service import *
|
85
mealie/services/cookbook/cookbook_service.py
Normal file
85
mealie/services/cookbook/cookbook_service.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, SaveCookBook
|
||||||
|
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||||
|
from mealie.services.events import create_group_event
|
||||||
|
|
||||||
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CookbookService(BaseHttpService[str, str]):
|
||||||
|
"""
|
||||||
|
Class Methods:
|
||||||
|
`read_existing`: Reads an existing recipe from the database.
|
||||||
|
`write_existing`: Updates an existing recipe in the database.
|
||||||
|
`base`: Requires write permissions, but doesn't perform recipe checks
|
||||||
|
"""
|
||||||
|
|
||||||
|
event_func = create_group_event
|
||||||
|
cookbook: ReadCookBook # Required for proper type hints
|
||||||
|
|
||||||
|
_group_id_cache = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_id(self):
|
||||||
|
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
|
||||||
|
if not self._group_id_cache:
|
||||||
|
group = self.db.groups.get(self.session, self.user.group, "name")
|
||||||
|
print(group)
|
||||||
|
self._group_id_cache = group.id
|
||||||
|
return self._group_id_cache
|
||||||
|
|
||||||
|
def assert_existing(self, id: str):
|
||||||
|
self.populate_cookbook(id)
|
||||||
|
|
||||||
|
if not self.cookbook:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if self.cookbook.group_id != self.group_id:
|
||||||
|
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def populate_cookbook(self, id):
|
||||||
|
self.cookbook = self.db.cookbooks.get(self.session, id)
|
||||||
|
|
||||||
|
def get_all(self) -> list[ReadCookBook]:
|
||||||
|
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
|
||||||
|
items.sort(key=lambda x: x.position)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def create_one(self, data: CreateCookBook) -> ReadCookBook:
|
||||||
|
try:
|
||||||
|
self.cookbook = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
||||||
|
except Exception as ex:
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_400_BAD_REQUEST, detail={"message": "PAGE_CREATION_ERROR", "exception": str(ex)}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.cookbook
|
||||||
|
|
||||||
|
def update_one(self, data: CreateCookBook, id: int = None) -> ReadCookBook:
|
||||||
|
if not self.cookbook:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_id = id or self.cookbook.id
|
||||||
|
self.cookbook = self.db.cookbooks.update(self.session, target_id, data)
|
||||||
|
|
||||||
|
return self.cookbook
|
||||||
|
|
||||||
|
def update_many(self, data: list[ReadCookBook]) -> list[ReadCookBook]:
|
||||||
|
updated = []
|
||||||
|
|
||||||
|
for cookbook in data:
|
||||||
|
cb = self.db.cookbooks.update(self.session, cookbook.id, cookbook)
|
||||||
|
updated.append(cb)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
|
def delete_one(self, id: int = None) -> ReadCookBook:
|
||||||
|
if not self.cookbook:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_id = id or self.cookbook.id
|
||||||
|
self.cookbook = self.db.cookbooks.delete(self.session, target_id)
|
||||||
|
|
||||||
|
return self.cookbook
|
@ -5,7 +5,7 @@ from typing import Union
|
|||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from mealie.core.dependencies.grouped import WriteDeps
|
from mealie.core.dependencies.grouped import ReadDeps, WriteDeps
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
||||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||||
@ -21,6 +21,7 @@ class RecipeService(BaseHttpService[str, str]):
|
|||||||
`write_existing`: Updates an existing recipe in the database.
|
`write_existing`: Updates an existing recipe in the database.
|
||||||
`base`: Requires write permissions, but doesn't perform recipe checks
|
`base`: Requires write permissions, but doesn't perform recipe checks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event_func = create_recipe_event
|
event_func = create_recipe_event
|
||||||
recipe: Recipe # Required for proper type hints
|
recipe: Recipe # Required for proper type hints
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ class RecipeService(BaseHttpService[str, str]):
|
|||||||
return super().write_existing(slug, deps)
|
return super().write_existing(slug, deps)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read_existing(cls, slug: str, deps: WriteDeps = Depends()):
|
def read_existing(cls, slug: str, deps: ReadDeps = Depends()):
|
||||||
return super().write_existing(slug, deps)
|
return super().write_existing(slug, deps)
|
||||||
|
|
||||||
def assert_existing(self, slug: str):
|
def assert_existing(self, slug: str):
|
||||||
|
@ -3,7 +3,7 @@ import unicodedata
|
|||||||
|
|
||||||
replace_abbreviations = {
|
replace_abbreviations = {
|
||||||
"cup ": "cup ",
|
"cup ": "cup ",
|
||||||
"g ": "gram ",
|
" g ": "gram ",
|
||||||
"kg ": "kilogram ",
|
"kg ": "kilogram ",
|
||||||
"lb ": "pound ",
|
"lb ": "pound ",
|
||||||
"ml ": "milliliter ",
|
"ml ": "milliliter ",
|
||||||
|
@ -55,8 +55,8 @@ def _exec_crf_test(input_text):
|
|||||||
|
|
||||||
|
|
||||||
def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]):
|
def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]):
|
||||||
|
print(list_of_ingrdeint_text)
|
||||||
crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text])
|
crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text])
|
||||||
|
|
||||||
crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))]
|
crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))]
|
||||||
|
|
||||||
for model in crf_models:
|
for model in crf_models:
|
||||||
|
@ -11,6 +11,7 @@ def cleanUnicodeFractions(s):
|
|||||||
"""
|
"""
|
||||||
Replace unicode fractions with ascii representation, preceded by a
|
Replace unicode fractions with ascii representation, preceded by a
|
||||||
space.
|
space.
|
||||||
|
|
||||||
"1\x215e" => "1 7/8"
|
"1\x215e" => "1 7/8"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ def unclump(s):
|
|||||||
|
|
||||||
def normalizeToken(s):
|
def normalizeToken(s):
|
||||||
"""
|
"""
|
||||||
TODO: FIX THIS. We used to use the pattern.en package to singularize words, but
|
ToDo: FIX THIS. We used to use the pattern.en package to singularize words, but
|
||||||
in the name of simple deployments, we took it out. We should fix this at some
|
in the name of simple deployments, we took it out. We should fix this at some
|
||||||
point.
|
point.
|
||||||
"""
|
"""
|
||||||
@ -133,12 +134,13 @@ def insideParenthesis(token, tokens):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
line = " ".join(tokens)
|
line = " ".join(tokens)
|
||||||
return re.match(r".*\(.*" + re.escape(token) + r".*\).*", line) is not None
|
return re.match(r".*\(.*" + re.escape(token) + ".*\).*", line) is not None
|
||||||
|
|
||||||
|
|
||||||
def displayIngredient(ingredient):
|
def displayIngredient(ingredient):
|
||||||
"""
|
"""
|
||||||
Format a list of (tag, [tokens]) tuples as an HTML string for display.
|
Format a list of (tag, [tokens]) tuples as an HTML string for display.
|
||||||
|
|
||||||
displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])])
|
displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])])
|
||||||
# => <span class='qty'>1</span> <span class='name'>cat pie</span>
|
# => <span class='qty'>1</span> <span class='name'>cat pie</span>
|
||||||
"""
|
"""
|
||||||
@ -220,21 +222,7 @@ def import_data(lines):
|
|||||||
|
|
||||||
# turn B-NAME/123 back into "name"
|
# turn B-NAME/123 back into "name"
|
||||||
tag, confidence = re.split(r"/", columns[-1], 1)
|
tag, confidence = re.split(r"/", columns[-1], 1)
|
||||||
tag = re.sub(r"^[BI]\-", "", tag).lower()
|
tag = re.sub("^[BI]\-", "", tag).lower()
|
||||||
|
|
||||||
# TODO: Integrate Confidence into API Response
|
|
||||||
print("Confidence", confidence)
|
|
||||||
|
|
||||||
# new token
|
|
||||||
if prevTag != tag or token == "n/a":
|
|
||||||
display[-1].append((tag, [token]))
|
|
||||||
data[-1][tag] = []
|
|
||||||
prevTag = tag
|
|
||||||
|
|
||||||
# continuation
|
|
||||||
else:
|
|
||||||
display[-1][-1][1].append(token)
|
|
||||||
data[-1][tag].append(token)
|
|
||||||
|
|
||||||
# ---- DISPLAY ----
|
# ---- DISPLAY ----
|
||||||
# build a structure which groups each token by its tag, so we can
|
# build a structure which groups each token by its tag, so we can
|
||||||
|
Loading…
x
Reference in New Issue
Block a user