diff --git a/mealie/app.py b/mealie/app.py index 615fb3ba1..dab5f3117 100644 --- a/mealie/app.py +++ b/mealie/app.py @@ -12,7 +12,6 @@ from routes import ( setting_routes, static_routes, theme_routes, - user_routes, ) from routes.recipe import ( all_recipe_routes, @@ -22,18 +21,6 @@ from routes.recipe import ( ) from utils.logger import logger -""" -TODO: -- [x] Fix Duplicate Category -- [x] Fix category overflow -- [ ] Enable Database Name Versioning -- [ ] Finish Frontend Category Management - - [x] Delete Category - - [ ] Sort Sidebar A-Z -- [ ] Refactor Test Endpoints - Abstract to fixture? - - -""" app = FastAPI( title="Mealie", description="A place for all your recipes", @@ -51,6 +38,10 @@ def start_scheduler(): import services.scheduler.scheduled_jobs +def init_settings(): + import services.theme_services + + def api_routers(): # Recipes app.include_router(all_recipe_routes.router) @@ -64,8 +55,6 @@ def api_routers(): app.include_router(theme_routes.router) # Backups/Imports Routes app.include_router(backup_routes.router) - # User Routes - app.include_router(user_routes.router) # Migration Routes app.include_router(migration_routes.router) app.include_router(debug_routes.router) @@ -90,6 +79,7 @@ app.include_router(static_routes.router) # generate_api_docs(app) start_scheduler() +init_settings() if __name__ == "__main__": logger.info("-----SYSTEM STARTUP-----") diff --git a/mealie/db/database.py b/mealie/db/database.py index 93b26f10f..1d0d2b737 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -9,7 +9,6 @@ from db.sql.theme_models import SiteThemeModel """ # TODO - [ ] Abstract Classes to use save_new, and update from base models - - [x] Create Category and Tags Table with Many to Many relationship """ @@ -49,7 +48,7 @@ class _Settings(BaseDocument): self.primary_key = "name" self.sql_model = SiteSettingsModel - def save_new(self, session: Session, main: dict, webhooks: dict) -> str: + def create(self, session: Session, main: dict, webhooks: dict) -> str: new_settings = self.sql_model(main.get("name"), webhooks) session.add(new_settings) diff --git a/mealie/db/db_base.py b/mealie/db/db_base.py index 63727743d..5438d2aba 100644 --- a/mealie/db/db_base.py +++ b/mealie/db/db_base.py @@ -106,7 +106,7 @@ class BaseDocument: return db_entry - def save_new(self, session: Session, document: dict) -> dict: + def create(self, session: Session, document: dict) -> dict: """Creates a new database entry for the given SQL Alchemy Model. Args: \n diff --git a/mealie/models/meal_models.py b/mealie/models/meal_models.py new file mode 100644 index 000000000..71f369a7c --- /dev/null +++ b/mealie/models/meal_models.py @@ -0,0 +1,38 @@ +from datetime import date +from typing import List, Optional + +from pydantic import BaseModel + + +class Meal(BaseModel): + slug: Optional[str] + name: Optional[str] + date: date + dateText: str + image: Optional[str] + description: Optional[str] + + +class MealData(BaseModel): + name: Optional[str] + slug: str + dateText: str + + +class MealPlan(BaseModel): + uid: Optional[str] + startDate: date + endDate: date + meals: List[Meal] + + class Config: + schema_extra = { + "example": { + "startDate": date.today(), + "endDate": date.today(), + "meals": [ + {"slug": "Packed Mac and Cheese", "date": date.today()}, + {"slug": "Eggs and Toast", "date": date.today()}, + ], + } + } diff --git a/mealie/models/recipe_models.py b/mealie/models/recipe_models.py index c316516e8..f1c7e59d9 100644 --- a/mealie/models/recipe_models.py +++ b/mealie/models/recipe_models.py @@ -1,39 +1,80 @@ -from typing import List, Optional -from pydantic.main import BaseModel +import datetime +from typing import Any, List, Optional + +from pydantic import BaseModel, validator +from slugify import slugify +class RecipeNote(BaseModel): + title: str + text: str +class RecipeStep(BaseModel): + text: str -class AllRecipeResponse(BaseModel): - + +class Recipe(BaseModel): + # Standard Schema + name: str + description: Optional[str] + image: Optional[Any] + recipeYield: Optional[str] + recipeIngredient: Optional[list] + recipeInstructions: Optional[list] + + totalTime: Optional[str] = None + prepTime: Optional[str] = None + performTime: Optional[str] = None + + # Mealie Specific + slug: Optional[str] = "" + categories: Optional[List[str]] = [] + tags: Optional[List[str]] = [] + dateAdded: Optional[datetime.date] + notes: Optional[List[RecipeNote]] = [] + rating: Optional[int] + orgURL: Optional[str] + extras: Optional[dict] = {} class Config: schema_extra = { - "example": [ - { - "slug": "crockpot-buffalo-chicken", - "image": "crockpot-buffalo-chicken.jpg", - "name": "Crockpot Buffalo Chicken", - }, - { - "slug": "downtown-marinade", - "image": "downtown-marinade.jpg", - "name": "Downtown Marinade", - }, - { - "slug": "detroit-style-pepperoni-pizza", - "image": "detroit-style-pepperoni-pizza.jpg", - "name": "Detroit-Style Pepperoni Pizza", - }, - { - "slug": "crispy-carrots", - "image": "crispy-carrots.jpg", - "name": "Crispy Carrots", - }, - ] + "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", + "recipeYield": "4 Servings", + "recipeIngredient": [ + "1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)", + "Kosher salt, freshly ground pepper", + "3 Tbsp. unsalted butter, divided", + ], + "recipeInstructions": [ + { + "text": "Season chicken with salt and pepper.", + }, + ], + "slug": "chicken-and-rice-with-leeks-and-salsa-verde", + "tags": ["favorite", "yummy!"], + "categories": ["Dinner", "Pasta"], + "notes": [{"title": "Watch Out!", "text": "Prep the day before!"}], + "orgURL": "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): + name: str = values["name"] + calc_slug: str = slugify(name) + + if slug == calc_slug: + return slug + else: + slug = calc_slug + return slug + class AllRecipeRequest(BaseModel): properties: List[str] diff --git a/mealie/models/settings_models.py b/mealie/models/settings_models.py new file mode 100644 index 000000000..6e8049ea6 --- /dev/null +++ b/mealie/models/settings_models.py @@ -0,0 +1,26 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class Webhooks(BaseModel): + webhookTime: str = "00:00" + webhookURLs: Optional[List[str]] = [] + enabled: bool = False + + +class SiteSettings(BaseModel): + name: str = "main" + webhooks: Webhooks + + class Config: + schema_extra = { + "example": { + "name": "main", + "webhooks": { + "webhookTime": "00:00", + "webhookURLs": ["https://mywebhookurl.com/webhook"], + "enable": False, + }, + } + } diff --git a/mealie/models/theme_models.py b/mealie/models/theme_models.py new file mode 100644 index 000000000..6b65da6f2 --- /dev/null +++ b/mealie/models/theme_models.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel + +class Colors(BaseModel): + primary: str + accent: str + secondary: str + success: str + info: str + warning: str + error: str + + +class SiteTheme(BaseModel): + name: str + colors: Colors + + class Config: + schema_extra = { + "example": { + "name": "default", + "colors": { + "primary": "#E58325", + "accent": "#00457A", + "secondary": "#973542", + "success": "#5AB1BB", + "info": "#4990BA", + "warning": "#FF4081", + "error": "#EF5350", + }, + } + } \ No newline at end of file diff --git a/mealie/models/user_models.py b/mealie/models/user_models.py deleted file mode 100644 index 81ac51d92..000000000 --- a/mealie/models/user_models.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - - -class User(BaseModel): - username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None diff --git a/mealie/routes/setting_routes.py b/mealie/routes/setting_routes.py index 414f92fbf..c8d190dce 100644 --- a/mealie/routes/setting_routes.py +++ b/mealie/routes/setting_routes.py @@ -1,6 +1,7 @@ +from db.database import db from db.db_setup import generate_session from fastapi import APIRouter, Depends -from services.settings_services import SiteSettings +from models.settings_models import SiteSettings from sqlalchemy.orm.session import Session from utils.post_webhooks import post_webhooks from utils.snackbar import SnackResponse @@ -12,7 +13,7 @@ router = APIRouter(prefix="/api/site-settings", tags=["Settings"]) def get_main_settings(session: Session = Depends(generate_session)): """ Returns basic site settings """ - return SiteSettings.get_site_settings(session) + return db.settings.get(session, "main") @router.post("/webhooks/test") @@ -25,6 +26,6 @@ def test_webhooks(): @router.put("") def update_settings(data: SiteSettings, session: Session = Depends(generate_session)): """ Returns Site Settings """ - data.update(session) + db.settings.update(session, "main", data.dict()) return SnackResponse.success("Settings Updated") diff --git a/mealie/routes/theme_routes.py b/mealie/routes/theme_routes.py index d656fb595..1d9646dd4 100644 --- a/mealie/routes/theme_routes.py +++ b/mealie/routes/theme_routes.py @@ -1,8 +1,9 @@ from db.db_setup import generate_session from fastapi import APIRouter, Depends -from services.settings_services import SiteTheme +from models.theme_models import SiteTheme from sqlalchemy.orm.session import Session from utils.snackbar import SnackResponse +from db.database import db router = APIRouter(prefix="/api", tags=["Themes"]) @@ -11,13 +12,13 @@ router = APIRouter(prefix="/api", tags=["Themes"]) def get_all_themes(session: Session = Depends(generate_session)): """ Returns all site themes """ - return SiteTheme.get_all(session) + return db.themes.get_all(session) @router.post("/themes/create") def create_theme(data: SiteTheme, session: Session = Depends(generate_session)): """ Creates a site color theme database entry """ - data.save_to_db(session) + db.themes.create(session, data.dict()) return SnackResponse.success("Theme Saved") @@ -25,7 +26,7 @@ def create_theme(data: SiteTheme, session: Session = Depends(generate_session)): @router.get("/themes/{theme_name}") def get_single_theme(theme_name: str, session: Session = Depends(generate_session)): """ Returns a named theme """ - return SiteTheme.get_by_name(session, theme_name) + return db.themes.get(session, theme_name) @router.put("/themes/{theme_name}") @@ -33,7 +34,7 @@ def update_theme( theme_name: str, data: SiteTheme, session: Session = Depends(generate_session) ): """ Update a theme database entry """ - data.update_document(session) + db.themes.update(session, theme_name, data.dict()) return SnackResponse.info(f"Theme Updated: {theme_name}") @@ -41,6 +42,6 @@ def update_theme( @router.delete("/themes/{theme_name}") def delete_theme(theme_name: str, session: Session = Depends(generate_session)): """ Deletes theme from the database """ - SiteTheme.delete_theme(session, theme_name) + db.themes.delete(session, theme_name) return SnackResponse.error(f"Theme Deleted: {theme_name}") diff --git a/mealie/routes/user_routes.py b/mealie/routes/user_routes.py deleted file mode 100644 index 5050fbbb4..000000000 --- a/mealie/routes/user_routes.py +++ /dev/null @@ -1,33 +0,0 @@ -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordRequestForm - -# from fastapi_login import LoginManager -# from fastapi_login.exceptions import InvalidCredentialsException - -router = APIRouter() - -# SECRET = "876cfb59db03d9c17cefec967b00255d3f7d93a823e5dc2a" -# manager = LoginManager(SECRET, tokenUrl="/api/auth/token") - -# fake_db = {"johndoe@e.mail": {"password": "hunter2"}} - - -# @manager.user_loader -# def load_user(email: str): # could also be an asynchronous function -# user = fake_db.get(email) -# return user - - -# @router.post("/api/auth/token", tags=["User Gen"]) -# def login(data: OAuth2PasswordRequestForm = Depends()): -# email = data.username -# password = data.password - -# user = load_user(email) # we are using the same function to retrieve the user -# if not user: -# raise InvalidCredentialsException # you can also use your own HTTPException -# elif password != user["password"]: -# raise InvalidCredentialsException - -# access_token = manager.create_access_token(data=dict(sub=email)) -# return {"access_token": access_token, "token_type": "bearer"} diff --git a/mealie/services/backups/exports.py b/mealie/services/backups/exports.py index fc6b7d3a3..8c53e3d70 100644 --- a/mealie/services/backups/exports.py +++ b/mealie/services/backups/exports.py @@ -4,11 +4,11 @@ from datetime import datetime from pathlib import Path from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR +from db.database import db from db.db_setup import create_session from jinja2 import Template from services.meal_services import MealPlan from services.recipe_services import Recipe -from services.settings_services import SiteSettings, SiteTheme from utils.logger import logger @@ -88,20 +88,18 @@ class ExportDatabase: shutil.copy(file, self.img_dir.joinpath(file.name)) def export_settings(self): - all_settings = SiteSettings.get_site_settings(self.session) + all_settings = db.settings.get(self.session, "main") out_file = self.settings_dir.joinpath("settings.json") - ExportDatabase._write_json_file(all_settings.dict(), out_file) + ExportDatabase._write_json_file(all_settings, out_file) def export_themes(self): - all_themes = SiteTheme.get_all(self.session) + all_themes = db.themes.get_all(self.session) if all_themes: - all_themes = [x.dict() for x in all_themes] out_file = self.themes_dir.joinpath("themes.json") ExportDatabase._write_json_file(all_themes, out_file) - def export_meals( - self, - ): #! Problem Parseing Datetime Objects... May come back to this + def export_meals(self): + #! Problem Parseing Datetime Objects... May come back to this meal_plans = MealPlan.get_all(self.session) if meal_plans: meal_plans = [x.dict() for x in meal_plans] diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py index 9e5ca7c83..e666efa1f 100644 --- a/mealie/services/backups/imports.py +++ b/mealie/services/backups/imports.py @@ -1,12 +1,15 @@ import json import shutil import zipfile +from logging import error from pathlib import Path from typing import List from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR +from db.database import db +from models.theme_models import SiteTheme from services.recipe_services import Recipe -from services.settings_services import SiteSettings, SiteTheme +from services.settings_services import SiteSettings from sqlalchemy.orm.session import Session from utils.logger import logger @@ -54,6 +57,7 @@ class ImportDatabase: raise Exception("Import file does not exist") def run(self): + report = {} if self.imp_recipes: report = self.import_recipes() if self.imp_settings: @@ -128,11 +132,13 @@ class ImportDatabase: themes_file = self.import_dir.joinpath("themes", "themes.json") with open(themes_file, "r") as f: - themes: list = json.loads(f.read()) + themes: list[dict] = json.loads(f.read()) for theme in themes: + if theme.get("name") == "default": + continue new_theme = SiteTheme(**theme) try: - new_theme.save_to_db(self.session) + db.themes.create(self.session, new_theme.dict()) except: logger.info(f"Unable Import Theme {new_theme.name}") @@ -142,9 +148,7 @@ class ImportDatabase: with open(settings_file, "r") as f: settings: dict = json.loads(f.read()) - settings = SiteSettings(**settings) - - settings.update(self.session) + db.settings.update(self.session, settings) def clean_up(self): shutil.rmtree(TEMP_DIR) diff --git a/mealie/services/meal_services.py b/mealie/services/meal_services.py index 6611bb738..5579ff51f 100644 --- a/mealie/services/meal_services.py +++ b/mealie/services/meal_services.py @@ -8,19 +8,6 @@ from sqlalchemy.orm.session import Session from services.recipe_services import Recipe -CWD = Path(__file__).parent -THIS_WEEK = CWD.parent.joinpath("data", "meal_plan", "this_week.json") -NEXT_WEEK = CWD.parent.joinpath("data", "meal_plan", "next_week.json") -WEEKDAYS = [ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", -] - class Meal(BaseModel): slug: Optional[str] @@ -81,7 +68,7 @@ class MealPlan(BaseModel): self.meals = meals def save_to_db(self, session: Session): - db.meals.save_new(session, self.dict()) + db.meals.create(session, self.dict()) @staticmethod def get_all(session: Session) -> List: diff --git a/mealie/services/recipe_services.py b/mealie/services/recipe_services.py index 0946e20b1..f95cb7291 100644 --- a/mealie/services/recipe_services.py +++ b/mealie/services/recipe_services.py @@ -1,5 +1,4 @@ import datetime -import json from pathlib import Path from typing import Any, List, Optional @@ -98,7 +97,7 @@ class Recipe(BaseModel): except: recipe_dict["image"] = "no image" - recipe_doc = db.recipes.save_new(session, recipe_dict) + recipe_doc = db.recipes.create(session, recipe_dict) recipe = Recipe(**recipe_doc) return recipe.slug diff --git a/mealie/services/scheduler/scheduled_jobs.py b/mealie/services/scheduler/scheduled_jobs.py index b273a8446..ea93fb202 100644 --- a/mealie/services/scheduler/scheduled_jobs.py +++ b/mealie/services/scheduler/scheduled_jobs.py @@ -3,8 +3,9 @@ from db.db_setup import create_session from services.backups.exports import auto_backup_job from services.scheduler.global_scheduler import scheduler from services.scheduler.scheduler_utils import Cron, cron_parser -from services.settings_services import SiteSettings from utils.logger import logger +from models.settings_models import SiteSettings +from db.database import db from utils.post_webhooks import post_webhooks @@ -15,7 +16,8 @@ def update_webhook_schedule(): poll the database for changes and reschedule the webhook time """ session = create_session() - settings = SiteSettings.get_site_settings(session=session) + settings = db.settings.get(session, "main") + settings = SiteSettings(**settings) time = cron_parser(settings.webhooks.webhookTime) job = JOB_STORE.get("webhooks") diff --git a/mealie/services/settings_services.py b/mealie/services/settings_services.py index e7f1d7c34..14d86052d 100644 --- a/mealie/services/settings_services.py +++ b/mealie/services/settings_services.py @@ -1,135 +1,6 @@ -from typing import List, Optional - from db.database import db from db.db_setup import create_session, sql_exists -from pydantic import BaseModel -from sqlalchemy.orm.session import Session -from utils.logger import logger - - -class Webhooks(BaseModel): - webhookTime: str = "00:00" - webhookURLs: Optional[List[str]] = [] - enabled: bool = False - - -class SiteSettings(BaseModel): - name: str = "main" - webhooks: Webhooks - - class Config: - schema_extra = { - "example": { - "name": "main", - "webhooks": { - "webhookTime": "00:00", - "webhookURLs": ["https://mywebhookurl.com/webhook"], - "enable": False, - }, - } - } - - @staticmethod - def get_all(session: Session): - db.settings.get_all(session) - - @classmethod - def get_site_settings(cls, session: Session): - try: - document = db.settings.get(session=session, match_value="main") - except: - webhooks = Webhooks() - default_entry = SiteSettings(name="main", webhooks=webhooks) - document = db.settings.save_new( - session, default_entry.dict(), webhooks.dict() - ) - - return cls(**document) - - def update(self, session: Session): - db.settings.update(session, "main", new_data=self.dict()) - - -class Colors(BaseModel): - primary: str - accent: str - secondary: str - success: str - info: str - warning: str - error: str - - -class SiteTheme(BaseModel): - name: str - colors: Colors - - class Config: - schema_extra = { - "example": { - "name": "default", - "colors": { - "primary": "#E58325", - "accent": "#00457A", - "secondary": "#973542", - "success": "#5AB1BB", - "info": "#4990BA", - "warning": "#FF4081", - "error": "#EF5350", - }, - } - } - - @classmethod - def get_by_name(cls, session: Session, theme_name): - db_entry = db.themes.get(session, theme_name) - name = db_entry.get("name") - colors = Colors(**db_entry.get("colors")) - - return cls(name=name, colors=colors) - - @staticmethod - def get_all(session: Session): - all_themes = db.themes.get_all(session) - for index, theme in enumerate(all_themes): - name = theme.get("name") - colors = Colors(**theme.get("colors")) - - all_themes[index] = SiteTheme(name=name, colors=colors) - - return all_themes - - def save_to_db(self, session: Session): - db.themes.save_new(session, self.dict()) - - def update_document(self, session: Session): - db.themes.update(session, self.name, self.dict()) - - @staticmethod - def delete_theme(session: Session, theme_name: str) -> str: - """ Removes the theme by name """ - db.themes.delete(session, theme_name) - - -def default_theme_init(): - default_colors = { - "primary": "#E58325", - "accent": "#00457A", - "secondary": "#973542", - "success": "#5AB1BB", - "info": "#4990BA", - "warning": "#FF4081", - "error": "#EF5350", - } - session = create_session() - try: - SiteTheme.get_by_name(session, "default") - logger.info("Default theme exists... skipping generation") - except: - logger.info("Generating Default Theme") - colors = Colors(**default_colors) - default_theme = SiteTheme(name="default", colors=colors) - default_theme.save_to_db(session) +from models.settings_models import SiteSettings, Webhooks def default_settings_init(): @@ -139,11 +10,10 @@ def default_settings_init(): except: webhooks = Webhooks() default_entry = SiteSettings(name="main", webhooks=webhooks) - document = db.settings.save_new(session, default_entry.dict(), webhooks.dict()) + document = db.settings.create(session, default_entry.dict(), webhooks.dict()) session.close() if not sql_exists: default_settings_init() - default_theme_init() diff --git a/mealie/services/theme_services.py b/mealie/services/theme_services.py new file mode 100644 index 000000000..12cd9c889 --- /dev/null +++ b/mealie/services/theme_services.py @@ -0,0 +1,28 @@ +from db.database import db +from db.db_setup import create_session, sql_exists +from utils.logger import logger + + +def default_theme_init(): + default_theme = { + "name": "default", + "colors": { + "primary": "#E58325", + "accent": "#00457A", + "secondary": "#973542", + "success": "#5AB1BB", + "info": "#4990BA", + "warning": "#FF4081", + "error": "#EF5350", + }, + } + session = create_session() + try: + db.themes.create(session, default_theme) + logger.info("Generating default theme...") + except: + logger.info("Default Theme Exists.. skipping generation") + + +if not sql_exists: + default_theme_init() diff --git a/mealie/utils/post_webhooks.py b/mealie/utils/post_webhooks.py index 56bbedc33..90520c51a 100644 --- a/mealie/utils/post_webhooks.py +++ b/mealie/utils/post_webhooks.py @@ -1,15 +1,17 @@ import json import requests +from db.database import db from db.db_setup import create_session +from models.settings_models import SiteSettings from services.meal_services import MealPlan from services.recipe_services import Recipe -from services.settings_services import SiteSettings def post_webhooks(): session = create_session() - all_settings = SiteSettings.get_site_settings(session) + all_settings = db.get(session, "main") + all_settings = SiteSettings(**all_settings) if all_settings.webhooks.enabled: todays_meal = Recipe.get_by_slug(MealPlan.today()).dict()