mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-15 04:13:11 -05:00
feature/finish-recipe-assets (#384)
* add features to readme
* Copy markdown reference
* prop as whole recipe
* parameter as url instead of query
* add card styling to editor
* move images to /recipes/{slug}/images
* add image to breaking changes
* fix delete and import errors
* fix debug/about response
* logger updates
* dashboard ui
* add server side events
* unorganized routes
* default slot
* add backup viewer to dashboard
* format
* add dialog to backup imports
* initial event support
* delete assets when removed
Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
7
mealie/routes/about/__init__.py
Normal file
7
mealie/routes/about/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .events import router as events_router
|
||||
|
||||
about_router = APIRouter(prefix="/api/about")
|
||||
|
||||
about_router.include_router(events_router)
|
||||
28
mealie/routes/about/events.py
Normal file
28
mealie/routes/about/events.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
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.events import EventsOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/events", tags=["App Events"])
|
||||
|
||||
|
||||
@router.get("", response_model=EventsOut)
|
||||
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
""" Get event from the Database """
|
||||
# Get Item
|
||||
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
|
||||
|
||||
|
||||
@router.delete("")
|
||||
async def delete_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
""" Get event from the Database """
|
||||
# Get Item
|
||||
return db.events.delete_all(session)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_event(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
""" Delete event from the Database """
|
||||
return db.events.delete(session, id)
|
||||
@@ -1,17 +1,21 @@
|
||||
import operator
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import create_file_token
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from mealie.services.backups import imports
|
||||
from mealie.services.backups.exports import backup_all
|
||||
from mealie.services.events import create_backup_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depends(get_current_user)])
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@router.get("/available", response_model=Imports)
|
||||
@@ -43,8 +47,10 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
|
||||
export_users=data.options.users,
|
||||
export_groups=data.options.groups,
|
||||
)
|
||||
create_backup_event("Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session)
|
||||
return {"export_path": export_path}
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@@ -72,7 +78,7 @@ async def download_backup_file(file_name: str):
|
||||
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
|
||||
""" Import a database backup file generated from Mealie. """
|
||||
|
||||
return imports.import_database(
|
||||
db_import = imports.import_database(
|
||||
session=session,
|
||||
archive=import_data.name,
|
||||
import_recipes=import_data.recipes,
|
||||
@@ -84,6 +90,8 @@ def import_database(file_name: str, import_data: ImportJob, session: Session = D
|
||||
force_import=import_data.force,
|
||||
rebase=import_data.rebase,
|
||||
)
|
||||
create_backup_event("Database Restore", f"Restored Database File {file_name}", session)
|
||||
return db_import
|
||||
|
||||
|
||||
@router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
|
||||
|
||||
@@ -2,8 +2,11 @@ from fastapi import APIRouter, Depends
|
||||
from mealie.core.config import APP_VERSION, app_dirs, settings
|
||||
from mealie.core.root_logger import LOGGER_FILE
|
||||
from mealie.core.security import create_file_token
|
||||
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.debug import AppInfo, DebugInfo
|
||||
from mealie.schema.about import AppInfo, AppStatistics, DebugInfo
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
|
||||
@@ -18,11 +21,23 @@ async def get_debug_info(current_user=Depends(get_current_user)):
|
||||
demo_status=settings.IS_DEMO,
|
||||
api_port=settings.API_PORT,
|
||||
api_docs=settings.API_DOCS,
|
||||
db_type=settings.DB_ENGINE,
|
||||
db_url=settings.DB_URL,
|
||||
default_group=settings.DEFAULT_GROUP,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/statistics")
|
||||
async def get_app_statistics(session: Session = Depends(generate_session)):
|
||||
return AppStatistics(
|
||||
total_recipes=db.recipes.count_all(session),
|
||||
uncategorized_recipes=db.recipes.count_uncategorized(session),
|
||||
untagged_recipes=db.recipes.count_untagged(session),
|
||||
total_users=db.users.count_all(session),
|
||||
total_groups=db.groups.count_all(session),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
async def get_mealie_version():
|
||||
""" Returns the current version of mealie"""
|
||||
|
||||
@@ -88,7 +88,7 @@ def get_todays_image(session: Session = Depends(generate_session), group_name: s
|
||||
recipe = get_todays_meal(session, group_in_db)
|
||||
|
||||
if recipe:
|
||||
recipe_image = image.read_image(recipe.slug, image_type=image.IMG_OPTIONS.ORIGINAL_IMAGE)
|
||||
recipe_image = recipe.image_dir.joinpath(image.ImageOptions.ORIGINAL_IMAGE)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
if recipe_image:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_assets, recipe_crud_routes, tag_routes
|
||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(all_recipe_routes.router)
|
||||
router.include_router(recipe_crud_routes.router)
|
||||
router.include_router(recipe_assets.router)
|
||||
router.include_router(recipe_media.router)
|
||||
router.include_router(category_routes.router)
|
||||
router.include_router(tag_routes.router)
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm.session import Session
|
||||
router = APIRouter(tags=["Query All Recipes"])
|
||||
|
||||
|
||||
@router.get("/api/recipes/summary")
|
||||
@router.get("/api/recipes/summary", response_model=list[RecipeSummary])
|
||||
async def get_recipe_summary(
|
||||
start=0,
|
||||
limit=9999,
|
||||
@@ -29,6 +29,16 @@ async def get_recipe_summary(
|
||||
return db.recipes.get_all(session, limit=limit, start=start, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.get("/api/recipes/summary/untagged", response_model=list[RecipeSummary])
|
||||
async def get_untagged_recipes(session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_untagged(session, False, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.get("/api/recipes/summary/uncategorized", response_model=list[RecipeSummary])
|
||||
async def get_uncategorized_recipes(session: Session = Depends(generate_session)):
|
||||
return db.recipes.count_uncategorized(session, False, override_schema=RecipeSummary)
|
||||
|
||||
|
||||
@router.post("/api/recipes/category")
|
||||
def filter_by_category(categories: list, session: Session = Depends(generate_session)):
|
||||
""" pass a list of categories and get a list of recipes associated with those categories """
|
||||
|
||||
@@ -4,7 +4,9 @@ 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.services.image.image import delete_image, rename_image, scrape_image, write_image
|
||||
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 sqlalchemy.orm.session import Session
|
||||
|
||||
@@ -21,6 +23,8 @@ def create_from_json(
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
recipe: Recipe = db.recipes.create(session, data.dict())
|
||||
|
||||
create_recipe_event("Recipe Created", f"Recipe '{recipe.name}' created", session=session)
|
||||
|
||||
return recipe.slug
|
||||
|
||||
|
||||
@@ -34,6 +38,7 @@ def parse_recipe_url(
|
||||
|
||||
recipe = create_from_url(url.url)
|
||||
recipe: Recipe = db.recipes.create(session, recipe.dict())
|
||||
create_recipe_event("Recipe Created (URL)", f"'{recipe.name}' by {current_user.full_name}", session=session)
|
||||
|
||||
return recipe.slug
|
||||
|
||||
@@ -57,8 +62,7 @@ def update_recipe(
|
||||
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
|
||||
print(recipe.assets)
|
||||
|
||||
if recipe_slug != recipe.slug:
|
||||
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
|
||||
check_assets(original_slug=recipe_slug, recipe=recipe)
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -75,8 +79,8 @@ def patch_recipe(
|
||||
recipe: Recipe = db.recipes.patch(
|
||||
session, recipe_slug, new_data=data.dict(exclude_unset=True, exclude_defaults=True)
|
||||
)
|
||||
if recipe_slug != recipe.slug:
|
||||
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
|
||||
|
||||
check_assets(original_slug=recipe_slug, recipe=recipe)
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -90,10 +94,10 @@ def delete_recipe(
|
||||
""" Deletes a recipe by slug """
|
||||
|
||||
try:
|
||||
delete_data = db.recipes.delete(session, recipe_slug)
|
||||
delete_image(recipe_slug)
|
||||
|
||||
return delete_data
|
||||
recipe: Recipe = db.recipes.delete(session, recipe_slug)
|
||||
delete_assets(recipe_slug=recipe_slug)
|
||||
create_recipe_event("Recipe Deleted", f"'{recipe.name}' deleted by {current_user.full_name}", session=session)
|
||||
return recipe
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from enum import Enum
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
||||
from fastapi.datastructures import UploadFile
|
||||
from mealie.core.config import app_dirs
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import get_current_user
|
||||
@@ -12,7 +11,7 @@ from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe Media"])
|
||||
router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"])
|
||||
|
||||
|
||||
class ImageType(str, Enum):
|
||||
@@ -21,25 +20,30 @@ class ImageType(str, Enum):
|
||||
tiny = "tiny-original.webp"
|
||||
|
||||
|
||||
@router.get("/image/{recipe_slug}/{file_name}")
|
||||
@router.get("/{recipe_slug}/image/{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 = app_dirs.IMG_DIR.joinpath(recipe_slug, file_name.value)
|
||||
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}/asset")
|
||||
async def get_recipe_asset(recipe_slug, file_name: str):
|
||||
@router.get("/{recipe_slug}/assets/{file_name}")
|
||||
async def get_recipe_asset(recipe_slug: str, file_name: str):
|
||||
""" Returns a recipe asset """
|
||||
file = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
|
||||
return FileResponse(file)
|
||||
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}/asset", response_model=RecipeAsset)
|
||||
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
||||
def upload_recipe_asset(
|
||||
recipe_slug: str,
|
||||
name: str = Form(...),
|
||||
@@ -52,8 +56,7 @@ def upload_recipe_asset(
|
||||
""" 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 = app_dirs.RECIPE_DATA_DIR.joinpath(recipe_slug, file_name)
|
||||
dest.parent.mkdir(exist_ok=True, parents=True)
|
||||
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
@@ -1,6 +1,6 @@
|
||||
import shutil
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, status, HTTPException
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from mealie.core import security
|
||||
from mealie.core.config import app_dirs, settings
|
||||
@@ -9,6 +9,7 @@ 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.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from mealie.services.events import create_sign_up_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
@@ -22,7 +23,7 @@ async def create_user(
|
||||
):
|
||||
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
|
||||
create_sign_up_event("User Created", f"Created by {current_user.full_name}", session=session)
|
||||
return db.users.create(session, new_user.dict())
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from mealie.core.security import get_password_hash
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from mealie.routes.deps import get_current_user
|
||||
from mealie.schema.sign_up import SignUpIn, SignUpOut, SignUpToken
|
||||
from mealie.schema.user import UserIn, UserInDB
|
||||
from mealie.services.events import create_sign_up_event
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
|
||||
@@ -20,9 +20,7 @@ async def get_all_open_sign_ups(
|
||||
):
|
||||
""" Returns a list of open sign up links """
|
||||
|
||||
all_sign_ups = db.sign_ups.get_all(session)
|
||||
|
||||
return all_sign_ups
|
||||
return db.sign_ups.get_all(session)
|
||||
|
||||
|
||||
@router.post("", response_model=SignUpToken)
|
||||
@@ -41,6 +39,7 @@ async def create_user_sign_up_key(
|
||||
"name": key_data.name,
|
||||
"admin": key_data.admin,
|
||||
}
|
||||
create_sign_up_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
|
||||
return db.sign_ups.create(session, sign_up)
|
||||
|
||||
|
||||
@@ -63,6 +62,7 @@ async def create_user_with_token(
|
||||
db.users.create(session, new_user.dict())
|
||||
|
||||
# DeleteToken
|
||||
create_sign_up_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user