mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-01 21:43:28 -05:00
Refactor/backend routers (#388)
* update router * update caddy file * setup depends in docker-fole * make changes for serving on subpath * set dev config * fix router signups * consolidate links * backup-functionality to dashboard * new user card * consolidate theme into profile * fix theme tests * fix pg tests * fix pg tests * remove unused import * mobile margin Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
||||
from mealie.core import root_logger
|
||||
from mealie.core.config import APP_VERSION, settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
|
||||
from mealie.routes.about import about_router
|
||||
from mealie.routes.groups import groups
|
||||
from mealie.routes.mealplans import mealplans
|
||||
from mealie.routes.recipe import router as recipe_router
|
||||
from mealie.routes.site_settings import all_settings
|
||||
from mealie.routes.users import users
|
||||
from mealie.routes.groups import groups_router
|
||||
from mealie.routes.mealplans import meal_plan_router
|
||||
from mealie.routes.media import media_router
|
||||
from mealie.routes.recipe import recipe_router
|
||||
from mealie.routes.site_settings import settings_router
|
||||
from mealie.routes.users import user_router
|
||||
from mealie.services.events import create_general_event
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
logger = get_logger()
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
@@ -29,15 +30,16 @@ def start_scheduler():
|
||||
|
||||
def api_routers():
|
||||
# Authentication
|
||||
app.include_router(users.router)
|
||||
app.include_router(groups.router)
|
||||
app.include_router(user_router)
|
||||
app.include_router(groups_router)
|
||||
# Recipes
|
||||
app.include_router(recipe_router)
|
||||
app.include_router(media_router)
|
||||
app.include_router(about_router)
|
||||
# Meal Routes
|
||||
app.include_router(mealplans.router)
|
||||
app.include_router(meal_plan_router)
|
||||
# Settings Routes
|
||||
app.include_router(all_settings.router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(theme_routes.router)
|
||||
# Backups/Imports Routes
|
||||
app.include_router(backup_routes.router)
|
||||
|
||||
@@ -93,7 +93,7 @@ class _Settings(BaseDocument):
|
||||
|
||||
class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.primary_key = "id"
|
||||
self.sql_model = SiteThemeModel
|
||||
self.schema = SiteTheme
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from mealie.db.models.model_base import SqlAlchemyBase
|
||||
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy.sql.sqltypes import Integer
|
||||
|
||||
|
||||
class SiteThemeModel(SqlAlchemyBase):
|
||||
class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "site_theme"
|
||||
name = sa.Column(sa.String, primary_key=True)
|
||||
id = sa.Column(Integer, primary_key=True, unique=True)
|
||||
name = sa.Column(sa.String, nullable=False, unique=True)
|
||||
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
|
||||
|
||||
def __init__(self, name: str, colors: dict, session=None) -> None:
|
||||
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
||||
self.name = name
|
||||
self.colors = ThemeColorsModel(**colors)
|
||||
|
||||
def update(self, session=None, name: str = None, colors: dict = None) -> dict:
|
||||
self.colors.update(**colors)
|
||||
return self
|
||||
|
||||
|
||||
class ThemeColorsModel(SqlAlchemyBase):
|
||||
class ThemeColorsModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "theme_colors"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
||||
@@ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase):
|
||||
info = sa.Column(sa.String)
|
||||
warning = sa.Column(sa.String)
|
||||
error = sa.Column(sa.String)
|
||||
|
||||
def update(
|
||||
self,
|
||||
primary: str = None,
|
||||
accent: str = None,
|
||||
secondary: str = None,
|
||||
success: str = None,
|
||||
info: str = None,
|
||||
warning: str = None,
|
||||
error: str = None,
|
||||
) -> None:
|
||||
self.primary = primary
|
||||
self.accent = accent
|
||||
self.secondary = secondary
|
||||
self.success = success
|
||||
self.info = info
|
||||
self.warning = warning
|
||||
self.error = error
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .events import router as events_router
|
||||
from . import events
|
||||
|
||||
about_router = APIRouter(prefix="/api/about")
|
||||
|
||||
about_router.include_router(events_router)
|
||||
about_router.include_router(events.router)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import crud, groups
|
||||
|
||||
groups_router = APIRouter()
|
||||
|
||||
groups_router.include_router(crud.router)
|
||||
groups_router.include_router(groups.router)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import crud, helpers, mealplans
|
||||
|
||||
meal_plan_router = APIRouter()
|
||||
|
||||
meal_plan_router.include_router(crud.router)
|
||||
meal_plan_router.include_router(helpers.router)
|
||||
meal_plan_router.include_router(mealplans.router)
|
||||
|
||||
7
mealie/routes/media/__init__.py
Normal file
7
mealie/routes/media/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import recipe
|
||||
|
||||
media_router = APIRouter(prefix="/api/media", tags=["Site Media"])
|
||||
|
||||
media_router.include_router(recipe.router)
|
||||
41
mealie/routes/media/recipe.py
Normal file
41
mealie/routes/media/recipe.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from mealie.schema.recipe import Recipe
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
"""
|
||||
These routes are for development only! These assets are served by Caddy when not
|
||||
in development mode. If you make changes, be sure to test the production container.
|
||||
"""
|
||||
|
||||
router = APIRouter(prefix="/recipes")
|
||||
|
||||
|
||||
class ImageType(str, Enum):
|
||||
original = "original.webp"
|
||||
small = "min-original.webp"
|
||||
tiny = "tiny-original.webp"
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/images/{file_name}")
|
||||
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
|
||||
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
||||
and should not hit the API in production"""
|
||||
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
|
||||
|
||||
if recipe_image:
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/assets/{file_name}")
|
||||
async def get_recipe_asset(recipe_slug: str, file_name: str):
|
||||
""" Returns a recipe asset """
|
||||
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||
|
||||
try:
|
||||
return FileResponse(file)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
0
mealie/routes/media/user.py
Normal file
0
mealie/routes/media/user.py
Normal file
@@ -1,10 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes
|
||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
||||
|
||||
router = APIRouter()
|
||||
recipe_router = APIRouter()
|
||||
|
||||
router.include_router(all_recipe_routes.router)
|
||||
router.include_router(recipe_crud_routes.router)
|
||||
router.include_router(recipe_media.router)
|
||||
router.include_router(category_routes.router)
|
||||
router.include_router(tag_routes.router)
|
||||
recipe_router.include_router(all_recipe_routes.router)
|
||||
recipe_router.include_router(recipe_crud_routes.router)
|
||||
recipe_router.include_router(category_routes.router)
|
||||
recipe_router.include_router(tag_routes.router)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from shutil import copyfileobj
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
||||
from fastapi.datastructures import UploadFile
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.recipe import Recipe, RecipeURLIn
|
||||
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
||||
from mealie.services.events import create_recipe_event
|
||||
from mealie.services.image.image import scrape_image, write_image
|
||||
from mealie.services.recipe.media import check_assets, delete_assets
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||
@@ -126,3 +130,30 @@ def scrape_image_url(
|
||||
""" Removes an existing image and replaces it with the incoming file. """
|
||||
|
||||
scrape_image(url.url, recipe_slug)
|
||||
|
||||
|
||||
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
||||
def upload_recipe_asset(
|
||||
recipe_slug: str,
|
||||
name: str = Form(...),
|
||||
icon: str = Form(...),
|
||||
extension: str = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Upload a file to store as a recipe asset """
|
||||
file_name = slugify(name) + "." + extension
|
||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
||||
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
copyfileobj(file.file, buffer)
|
||||
|
||||
if not dest.is_file():
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
recipe: Recipe = db.recipes.get(session, recipe_slug)
|
||||
recipe.assets.append(asset_in)
|
||||
db.recipes.update(session, recipe_slug, recipe.dict())
|
||||
return asset_in
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import shutil
|
||||
from enum import Enum
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
||||
from fastapi.datastructures import UploadFile
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.recipe import Recipe, RecipeAsset
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"])
|
||||
|
||||
|
||||
class ImageType(str, Enum):
|
||||
original = "original.webp"
|
||||
small = "min-original.webp"
|
||||
tiny = "tiny-original.webp"
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/images/{file_name}")
|
||||
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
|
||||
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
||||
and should not hit the API in production"""
|
||||
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
|
||||
|
||||
if recipe_image:
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@router.get("/{recipe_slug}/assets/{file_name}")
|
||||
async def get_recipe_asset(recipe_slug: str, file_name: str):
|
||||
""" Returns a recipe asset """
|
||||
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||
|
||||
try:
|
||||
return FileResponse(file)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
||||
def upload_recipe_asset(
|
||||
recipe_slug: str,
|
||||
name: str = Form(...),
|
||||
icon: str = Form(...),
|
||||
extension: str = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Upload a file to store as a recipe asset """
|
||||
file_name = slugify(name) + "." + extension
|
||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
||||
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
if not dest.is_file():
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
recipe: Recipe = db.recipes.get(session, recipe_slug)
|
||||
recipe.assets.append(asset_in)
|
||||
db.recipes.update(session, recipe_slug, recipe.dict())
|
||||
return asset_in
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import all_settings, custom_pages, site_settings
|
||||
|
||||
settings_router = APIRouter()
|
||||
|
||||
settings_router.include_router(all_settings.router)
|
||||
settings_router.include_router(custom_pages.router)
|
||||
settings_router.include_router(site_settings.router)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, status, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
@@ -21,27 +21,27 @@ def create_theme(data: SiteTheme, session: Session = Depends(generate_session),
|
||||
db.themes.create(session, data.dict())
|
||||
|
||||
|
||||
@router.get("/themes/{theme_name}")
|
||||
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
|
||||
@router.get("/themes/{id}")
|
||||
def get_single_theme(id: int, session: Session = Depends(generate_session)):
|
||||
""" Returns a named theme """
|
||||
return db.themes.get(session, theme_name)
|
||||
return db.themes.get(session, id)
|
||||
|
||||
|
||||
@router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK)
|
||||
@router.put("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def update_theme(
|
||||
theme_name: str,
|
||||
id: int,
|
||||
data: SiteTheme,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" Update a theme database entry """
|
||||
db.themes.update(session, theme_name, data.dict())
|
||||
db.themes.update(session, id, data.dict())
|
||||
|
||||
|
||||
@router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK)
|
||||
def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
""" Deletes theme from the database """
|
||||
try:
|
||||
db.themes.delete(session, theme_name)
|
||||
db.themes.delete(session, id)
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from . import auth, crud, sign_up
|
||||
|
||||
user_router = APIRouter()
|
||||
|
||||
user_router.include_router(auth.router)
|
||||
user_router.include_router(sign_up.router)
|
||||
user_router.include_router(crud.router)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from fastapi import APIRouter
|
||||
from mealie.routes.users import auth, crud, sign_up
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(sign_up.router)
|
||||
router.include_router(auth.router)
|
||||
router.include_router(sign_up.router)
|
||||
router.include_router(crud.router)
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -5,7 +7,7 @@ class Colors(BaseModel):
|
||||
primary: str = "#E58325"
|
||||
accent: str = "#00457A"
|
||||
secondary: str = "#973542"
|
||||
success: str = "#4CAF50"
|
||||
success: str = "#43A047"
|
||||
info: str = "#4990BA"
|
||||
warning: str = "#FF4081"
|
||||
error: str = "#EF5350"
|
||||
@@ -15,6 +17,7 @@ class Colors(BaseModel):
|
||||
|
||||
|
||||
class SiteTheme(BaseModel):
|
||||
id: Optional[int]
|
||||
name: str = "default"
|
||||
colors: Colors = Colors()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user