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:
Hayden
2021-05-03 19:32:37 -08:00
committed by GitHub
parent f2d2b79a57
commit 5580d177c3
61 changed files with 1276 additions and 266 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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