theme + settings refactor

This commit is contained in:
hay-kot 2021-02-09 22:17:11 -09:00
commit 866e7de498
19 changed files with 236 additions and 262 deletions

View file

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

View file

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

View file

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

View file

@ -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()},
],
}
}

View file

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

View file

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

View file

@ -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",
},
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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