diff --git a/mealie/core/dependencies/grouped.py b/mealie/core/dependencies/grouped.py
index 6bfaefe763d8..75150779da6b 100644
--- a/mealie/core/dependencies/grouped.py
+++ b/mealie/core/dependencies/grouped.py
@@ -24,7 +24,7 @@ class ReadDeps:
user=Depends(is_logged_in),
):
self.session: Session = session
- self.bg_tasks: BackgroundTasks = background_tasks
+ self.bg_task: BackgroundTasks = background_tasks
self.user: bool = user
diff --git a/mealie/db/data_access_layer/_base_access_model.py b/mealie/db/data_access_layer/_base_access_model.py
index 257cb41ff544..33e90f593f78 100644
--- a/mealie/db/data_access_layer/_base_access_model.py
+++ b/mealie/db/data_access_layer/_base_access_model.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from typing import Callable, Generic, TypeVar, Union
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:
self.primary_key = primary_key
-
self.sql_model = sql_model
-
self.schema = schema
-
self.observers: list = []
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]
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.
-
- 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:
match_key = self.primary_key
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(
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
key is provided the class objects primary key will be used to match against.
Args:
- match_value (str): A value used to match against the key/value in the database \n
- match_key (str, optional): They key to match the value against. Defaults to None. \n
- limit (int, optional): A limit to returned responses. Defaults to 1. \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.
+ limit (int, optional): A limit to returned responses. Defaults to 1.
Returns:
dict or list[dict]:
"""
- if match_key is None:
- match_key = self.primary_key
+ match_key = match_key or self.primary_key
if any_case:
search_attr = getattr(self.sql_model, match_key)
diff --git a/mealie/db/data_access_layer/db_access.py b/mealie/db/data_access_layer/db_access.py
index 280b07403341..454fe057377a 100644
--- a/mealie/db/data_access_layer/db_access.py
+++ b/mealie/db/data_access_layer/db_access.py
@@ -3,6 +3,7 @@ from logging import getLogger
from sqlalchemy.orm.session import Session
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.group import Group
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.ingredient import IngredientFoodModel, IngredientUnitModel
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.sign_up import SignUp
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.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.events import EventNotificationIn
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
@@ -73,7 +74,6 @@ class DatabaseAccessLayer:
# Site
self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
- self.custom_pages = BaseAccessModel(DEFAULT_PK, CustomPage, CustomPageOut)
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
@@ -83,3 +83,4 @@ class DatabaseAccessLayer:
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
+ self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)
diff --git a/mealie/db/models/_model_utils.py b/mealie/db/models/_model_utils.py
index 0199b186deeb..9eaee5458ab2 100644
--- a/mealie/db/models/_model_utils.py
+++ b/mealie/db/models/_model_utils.py
@@ -4,7 +4,7 @@ from typing import Union
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 = []
updated_elems = []
@@ -75,8 +75,17 @@ def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
relation_cls = relationships[key].mapper.entity
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:
- instances = handle_one_to_many_list(relation_cls, val)
+ instances = handle_one_to_many_list(get_attr, relation_cls, val)
setattr(self, key, instances)
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:
if isinstance(val, dict):
- val = val.get("id")
+ val = val.get(get_attr)
if val is None:
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}")
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]
setattr(self, key, instances)
diff --git a/mealie/db/models/cookbook.py b/mealie/db/models/cookbook.py
new file mode 100644
index 000000000000..0e1cf187685f
--- /dev/null
+++ b/mealie/db/models/cookbook.py
@@ -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)
diff --git a/mealie/db/models/group.py b/mealie/db/models/group.py
index a0c943d814f9..0650686cce1f 100644
--- a/mealie/db/models/group.py
+++ b/mealie/db/models/group.py
@@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import settings
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
@@ -19,19 +20,9 @@ class Group(SqlAlchemyBase, BaseMixins):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
- mealplans = orm.relationship(
- "MealPlan",
- back_populates="group",
- single_parent=True,
- order_by="MealPlan.start_date",
- )
-
- shopping_lists = orm.relationship(
- "ShoppingList",
- back_populates="group",
- single_parent=True,
- )
-
+ mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
+ shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
+ cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
# Webhook Settings
diff --git a/mealie/db/models/recipe/category.py b/mealie/db/models/recipe/category.py
index 6ccfd1797f05..edcc67256d7c 100644
--- a/mealie/db/models/recipe/category.py
+++ b/mealie/db/models/recipe/category.py
@@ -29,10 +29,10 @@ recipes2categories = sa.Table(
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
)
-custom_pages2categories = sa.Table(
- "custom_pages2categories",
+cookbooks_to_categories = sa.Table(
+ "cookbooks_to_categories",
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")),
)
@@ -61,6 +61,7 @@ class Category(SqlAlchemyBase, BaseMixins):
if not session or not match_value:
return None
+ print(match_value)
slug = slugify(match_value)
result = session.query(Category).filter(Category.slug == slug).one_or_none()
diff --git a/mealie/db/models/settings.py b/mealie/db/models/settings.py
index ca9e1c3282a9..79464f0bf3a2 100644
--- a/mealie/db/models/settings.py
+++ b/mealie/db/models/settings.py
@@ -3,7 +3,7 @@ import sqlalchemy.orm as orm
from sqlalchemy.orm import Session
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):
@@ -33,21 +33,3 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
def update(self, *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)
diff --git a/mealie/routes/groups/__init__.py b/mealie/routes/groups/__init__.py
index 57bcfb8cf0a7..544459501099 100644
--- a/mealie/routes/groups/__init__.py
+++ b/mealie/routes/groups/__init__.py
@@ -1,8 +1,9 @@
from fastapi import APIRouter
-from . import crud
+from . import cookbooks, crud
router = APIRouter()
+router.include_router(cookbooks.user_router)
router.include_router(crud.user_router)
router.include_router(crud.admin_router)
diff --git a/mealie/routes/groups/cookbooks.py b/mealie/routes/groups/cookbooks.py
new file mode 100644
index 000000000000..52b339a5f24d
--- /dev/null
+++ b/mealie/routes/groups/cookbooks.py
@@ -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()
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index 544b9a9531a7..fd4cd4ba4050 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -31,13 +31,13 @@ def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existi
@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"""
return recipe_service.create_recipe(data).slug
@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 """
recipe = create_from_url(url.url)
diff --git a/mealie/schema/cookbook/__init__.py b/mealie/schema/cookbook/__init__.py
new file mode 100644
index 000000000000..3fae82e26ebc
--- /dev/null
+++ b/mealie/schema/cookbook/__init__.py
@@ -0,0 +1 @@
+from .cookbook import *
diff --git a/mealie/schema/cookbook/cookbook.py b/mealie/schema/cookbook/cookbook.py
new file mode 100644
index 000000000000..adffcba0e56b
--- /dev/null
+++ b/mealie/schema/cookbook/cookbook.py
@@ -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
diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py
index ac2287ed2a86..7a8b36997254 100644
--- a/mealie/schema/recipe/recipe_category.py
+++ b/mealie/schema/recipe/recipe_category.py
@@ -19,7 +19,6 @@ class CategoryBase(CategoryIn):
def getter_dict(_cls, name_orm):
return {
**GetterDict(name_orm),
- "total_recipes": len(name_orm.recipes),
}
diff --git a/mealie/schema/recipe_old/__init__.py b/mealie/schema/recipe_old/__init__.py
deleted file mode 100644
index 35d69062b42f..000000000000
--- a/mealie/schema/recipe_old/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .category import *
-from .comments import *
-from .helpers import *
-from .recipe import *
diff --git a/mealie/schema/recipe_old/category.py b/mealie/schema/recipe_old/category.py
deleted file mode 100644
index ac2287ed2a86..000000000000
--- a/mealie/schema/recipe_old/category.py
+++ /dev/null
@@ -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()
diff --git a/mealie/schema/recipe_old/comments.py b/mealie/schema/recipe_old/comments.py
deleted file mode 100644
index 6abd8121e013..000000000000
--- a/mealie/schema/recipe_old/comments.py
+++ /dev/null
@@ -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,
- }
diff --git a/mealie/schema/recipe_old/helpers.py b/mealie/schema/recipe_old/helpers.py
deleted file mode 100644
index 85e55e91a408..000000000000
--- a/mealie/schema/recipe_old/helpers.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from fastapi_camelcase import CamelModel
-
-
-class RecipeSlug(CamelModel):
- slug: str
diff --git a/mealie/schema/recipe_old/recipe.py b/mealie/schema/recipe_old/recipe.py
deleted file mode 100644
index c327c4c9bdd6..000000000000
--- a/mealie/schema/recipe_old/recipe.py
+++ /dev/null
@@ -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"}
diff --git a/mealie/schema/recipe_old/units_and_foods.py b/mealie/schema/recipe_old/units_and_foods.py
deleted file mode 100644
index 35a1a4106fad..000000000000
--- a/mealie/schema/recipe_old/units_and_foods.py
+++ /dev/null
@@ -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
diff --git a/mealie/services/base_http_service/base_http_service.py b/mealie/services/base_http_service/base_http_service.py
index 61f34512be5f..8d76fa1bcf2d 100644
--- a/mealie/services/base_http_service/base_http_service.py
+++ b/mealie/services/base_http_service/base_http_service.py
@@ -48,25 +48,19 @@ class BaseHttpService(Generic[T, D]):
def assert_existing(self, data: T) -> None:
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
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
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)
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
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.assert_existing(id)
return new_class
@classmethod
- def base(cls, deps: WriteDeps = Depends()):
- """A Base instance to be used as a router dependency
-
- Raises:
- HTTPException: 400 Bad Request
-
+ def public(cls, deps: ReadDeps = Depends()):
+ """
+ A Base instance to be used as a router dependency
"""
return cls(deps.session, deps.user, deps.bg_task)
- 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
+ def private(cls, deps: WriteDeps = Depends()):
+ """
+ A Base instance to be used as a router dependency
+ """
+ return cls(deps.session, deps.user, deps.bg_task)
diff --git a/mealie/services/cookbook/__init__.py b/mealie/services/cookbook/__init__.py
new file mode 100644
index 000000000000..3c4177f13202
--- /dev/null
+++ b/mealie/services/cookbook/__init__.py
@@ -0,0 +1 @@
+from .cookbook_service import *
diff --git a/mealie/services/cookbook/cookbook_service.py b/mealie/services/cookbook/cookbook_service.py
new file mode 100644
index 000000000000..2faef9c2f77c
--- /dev/null
+++ b/mealie/services/cookbook/cookbook_service.py
@@ -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
diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py
index fd4da0507eed..7f9f73830db1 100644
--- a/mealie/services/recipe/recipe_service.py
+++ b/mealie/services/recipe/recipe_service.py
@@ -5,7 +5,7 @@ from typing import Union
from fastapi import Depends, HTTPException, status
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.schema.recipe.recipe import CreateRecipe, Recipe
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.
`base`: Requires write permissions, but doesn't perform recipe checks
"""
+
event_func = create_recipe_event
recipe: Recipe # Required for proper type hints
@@ -29,7 +30,7 @@ class RecipeService(BaseHttpService[str, str]):
return super().write_existing(slug, deps)
@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)
def assert_existing(self, slug: str):
diff --git a/mealie/services/scraper/ingredient_nlp/pre_processor.py b/mealie/services/scraper/ingredient_nlp/pre_processor.py
index a6a5d4726c47..514fb187be1b 100644
--- a/mealie/services/scraper/ingredient_nlp/pre_processor.py
+++ b/mealie/services/scraper/ingredient_nlp/pre_processor.py
@@ -3,7 +3,7 @@ import unicodedata
replace_abbreviations = {
"cup ": "cup ",
- "g ": "gram ",
+ " g ": "gram ",
"kg ": "kilogram ",
"lb ": "pound ",
"ml ": "milliliter ",
diff --git a/mealie/services/scraper/ingredient_nlp/processor.py b/mealie/services/scraper/ingredient_nlp/processor.py
index 7e18709cf78a..6d7955b05e2d 100644
--- a/mealie/services/scraper/ingredient_nlp/processor.py
+++ b/mealie/services/scraper/ingredient_nlp/processor.py
@@ -55,8 +55,8 @@ def _exec_crf_test(input_text):
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_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))]
for model in crf_models:
diff --git a/mealie/services/scraper/ingredient_nlp/utils.py b/mealie/services/scraper/ingredient_nlp/utils.py
index 4b49d5908abc..4777cf1290b9 100644
--- a/mealie/services/scraper/ingredient_nlp/utils.py
+++ b/mealie/services/scraper/ingredient_nlp/utils.py
@@ -11,6 +11,7 @@ def cleanUnicodeFractions(s):
"""
Replace unicode fractions with ascii representation, preceded by a
space.
+
"1\x215e" => "1 7/8"
"""
@@ -47,7 +48,7 @@ def unclump(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
point.
"""
@@ -133,12 +134,13 @@ def insideParenthesis(token, tokens):
return True
else:
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):
"""
Format a list of (tag, [tokens]) tuples as an HTML string for display.
+
displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])])
# => 1 cat pie
"""
@@ -220,21 +222,7 @@ def import_data(lines):
# turn B-NAME/123 back into "name"
tag, confidence = re.split(r"/", columns[-1], 1)
- tag = re.sub(r"^[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)
+ tag = re.sub("^[BI]\-", "", tag).lower()
# ---- DISPLAY ----
# build a structure which groups each token by its tag, so we can