fix(backend): 🐛 Fix recipe page issues (#778)

* fix(backend): 🐛 Fix favorite assignment on backend

* fix(frontend): 🐛 fix printer button on recipe page

* style(frontend): 🚸 add user feadback on copy of recipe link

* fix(frontend): 🐛 Fix enableLandscape incorrect bindings to remove duplicate values

* feat(frontend):  add ingredient copy button for markdown list -[ ] format

* feat(frontend):  add remove prefix button to bulk entry

* fix(frontend): 🐛 disable random button when no recipes are present

* fix(frontend):  fix .zip download error

* fix(frontend): 🚸 close image dialog on upload/get

* fix(frontend): 🐛 fix assignment on creation for categories and tags

* feat(frontend):  Open editor on creation / fix edit button on main screen

* fix(frontend): 🐛 fix false negative regex match for urls on creationg page

* feat(frontend): 🚸 provide better user feadback when recipe exists

* feat(frontend):  lock bulk importer on submit

* remove zip from navigation

* fix(frontend):  rerender recipes on delete

Co-authored-by: Hayden K <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-11-04 18:15:23 -08:00
committed by GitHub
parent ec3b53cdc3
commit 9f8c61a75a
27 changed files with 323 additions and 163 deletions

View File

@@ -115,6 +115,23 @@ def validate_file_token(token: Optional[str] = None) -> Path:
return file_path
def validate_recipe_token(token: Optional[str] = None) -> str:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="could not validate file token",
)
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
slug = payload.get("slug")
except JWTError:
raise credentials_exception
return slug
async def temporary_zip_path() -> Path:
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")

View File

@@ -25,11 +25,16 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
def create_file_token(file_path: Path) -> bool:
def create_file_token(file_path: Path) -> str:
token_data = {"file": str(file_path)}
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
def create_recipe_slug_token(file_path: str) -> str:
token_data = {"slug": str(file_path)}
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
def authenticate_user(session, email: str, password: str) -> PrivateUser:
db = get_database(session)

View File

@@ -74,23 +74,23 @@ def handle_many_to_many(session, get_attr, relation_cls, all_elements: list[dict
return handle_one_to_many_list(session, get_attr, relation_cls, all_elements)
def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elements: list[dict]):
def handle_one_to_many_list(session: Session, get_attr, relation_cls, all_elements: list[dict] | list[str]):
elems_to_create: list[dict] = []
updated_elems: list[dict] = []
for elem in all_elements:
elem_id = elem.get(get_attr, None)
elem_id = elem.get(get_attr, None) if isinstance(elem, dict) else elem
existing_elem = session.query(relation_cls).filter_by(**{get_attr: elem_id}).one_or_none()
if existing_elem is None:
elems_to_create.append(elem)
continue
else:
elif isinstance(elem, dict):
for key, value in elem.items():
setattr(existing_elem, key, value)
updated_elems.append(existing_elem)
updated_elems.append(existing_elem)
new_elems = [safe_call(relation_cls, elem) for elem in elems_to_create]
return new_elems + updated_elems

View File

@@ -3,6 +3,7 @@ from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from mealie.core.config import get_app_settings
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import auto_init
from ..group import Group
from .user_to_favorite import users_to_favorites
@@ -56,41 +57,37 @@ class User(SqlAlchemyBase, BaseMixins):
favorite_recipes = orm.relationship("RecipeModel", secondary=users_to_favorites, back_populates="favorited_by")
def __init__(
self,
session,
full_name,
email,
password,
favorite_recipes: list[str] = None,
group: str = settings.DEFAULT_GROUP,
advanced=False,
**kwargs
) -> None:
group = group or settings.DEFAULT_GROUP
favorite_recipes = favorite_recipes or []
class Config:
exclude = {
"password",
"admin",
"can_manage",
"can_invite",
"can_organize",
"group",
"username",
}
@auto_init()
def __init__(self, session, full_name, password, group: str = settings.DEFAULT_GROUP, **kwargs) -> None:
self.group = Group.get_ref(session, group)
self.full_name = full_name
self.email = email
self.password = password
self.advanced = advanced
self.favorite_recipes = []
self.password = password
if self.username is None:
self.username = full_name
self._set_permissions(**kwargs)
def update(self, full_name, email, group, username, session=None, favorite_recipes=None, advanced=False, **kwargs):
favorite_recipes = favorite_recipes or []
@auto_init()
def update(self, full_name, email, group, username, session=None, **kwargs):
self.username = username
self.full_name = full_name
self.email = email
self.group = Group.get_ref(session, group)
self.advanced = advanced
if self.username is None:
self.username = full_name

View File

@@ -8,6 +8,7 @@ router = APIRouter()
router.include_router(all_recipe_routes.router, prefix=prefix, tags=["Recipe: Query All"])
router.include_router(recipe_export.user_router, prefix=prefix, tags=["Recipe: Exports"])
router.include_router(recipe_export.public_router, prefix=prefix, tags=["Recipe: Exports"])
router.include_router(recipe_crud_routes.user_router, prefix=prefix, tags=["Recipe: CRUD"])
router.include_router(image_and_assets.user_router, prefix=prefix, tags=["Recipe: Images and Assets"])
router.include_router(comments.router, prefix=prefix, tags=["Recipe: Comments"])

View File

@@ -1,18 +1,14 @@
from zipfile import ZipFile
from fastapi import Depends, File
from fastapi.datastructures import UploadFile
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
from mealie.core.dependencies import temporary_zip_path
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByUrl, Recipe, RecipeImageTypes
from mealie.schema.recipe import CreateRecipeByUrl, Recipe
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
from mealie.schema.server.tasks import ServerTaskNames
from mealie.services.recipe.recipe_service import RecipeService
@@ -109,23 +105,6 @@ def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existi
return recipe_service.item
@user_router.get("/{slug}/zip")
async def get_recipe_as_zip(
slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
):
""" Get a Recipe and It's Original Image as a Zip File """
db = get_database(session)
recipe: Recipe = db.recipes.get(slug)
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
with ZipFile(temp_path, "w") as myzip:
myzip.writestr(f"{slug}.json", recipe.json())
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)
return FileResponse(temp_path, filename=f"{slug}.zip")
@user_router.put("/{slug}")
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """

View File

@@ -1,14 +1,23 @@
from fastapi import Depends
from zipfile import ZipFile
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
from mealie.core.dependencies.dependencies import temporary_dir
from mealie.core.dependencies import temporary_zip_path
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
from mealie.core.root_logger import get_logger
from mealie.core.security import create_recipe_slug_token
from mealie.db.database import get_database
from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import Recipe, RecipeImageTypes
from mealie.services.recipe.recipe_service import RecipeService
from mealie.services.recipe.template_service import TemplateService
user_router = UserAPIRouter()
public_router = APIRouter()
logger = get_logger()
@@ -23,6 +32,12 @@ async def get_recipe_formats_and_templates(_: RecipeService = Depends(RecipeServ
return TemplateService().templates
@user_router.post("/{slug}/exports")
async def get_recipe_zip_token(slug: str):
""" Generates a recipe zip token to be used to download a recipe as a zip file """
return {"token": create_recipe_slug_token(slug)}
@user_router.get("/{slug}/exports", response_class=FileResponse)
def get_recipe_as_format(
template_name: str,
@@ -38,3 +53,28 @@ def get_recipe_as_format(
"""
file = recipe_service.render_template(temp_dir, template_name)
return FileResponse(file)
@public_router.get("/{slug}/exports/zip")
async def get_recipe_as_zip(
token: str,
slug: str,
session: Session = Depends(generate_session),
temp_path=Depends(temporary_zip_path),
):
""" Get a Recipe and It's Original Image as a Zip File """
slug = validate_recipe_token(token)
if slug != slug:
raise HTTPException(status_code=400, detail="Invalid Slug")
db = get_database(session)
recipe: Recipe = db.recipes.get(slug)
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
with ZipFile(temp_path, "w") as myzip:
myzip.writestr(f"{slug}.json", recipe.json())
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)
return FileResponse(temp_path, filename=f"{slug}.zip")

View File

@@ -26,9 +26,7 @@ def add_favorite(
):
""" Adds a Recipe to the users favorites """
assert_user_change_allowed(id, current_user)
current_user.favorite_recipes.append(slug)
db = get_database(session)
db.users.update(current_user.id, current_user)