Feature/event notifications (#399)

* additional server events

* sort 'recent recipes' by updated

* remove duplicate code

* fixes #396

* set color

* consolidate tag/category pages

* set colors

* list unorganized recipes

* cleanup old code

* remove flash message, switch to global snackbar

* cancel to close

* cleanup

* notifications first pass

* test notification

* complete notification feature

* use background tasks

* add url param

* update documentation

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-05-08 18:29:31 -08:00
committed by GitHub
parent 8923c1ecf8
commit 14b6ab7ec7
49 changed files with 875 additions and 355 deletions

View File

@@ -1,28 +1,97 @@
from fastapi import APIRouter, Depends
from http.client import HTTPException
from fastapi import APIRouter, Depends, status
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.events import EventsOut
from mealie.schema.event_notifications import EventNotificationIn, EventNotificationOut
from mealie.schema.events import EventsOut, TestEvent
from mealie.schema.user import UserInDB
from mealie.services.events import test_notification
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/events", tags=["App Events"])
logger = get_logger()
@router.get("", response_model=EventsOut)
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
async def get_events(session: Session = Depends(generate_session), current_user: UserInDB = 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)):
async def delete_events(
session: Session = Depends(generate_session), current_user: UserInDB = 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)):
async def delete_event(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event from the Database """
return db.events.delete(session, id)
@router.post("/notifications")
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create event_notification in the Database """
return db.event_notifications.create(session, event_data)
@router.post("/notifications/test")
async def test_notification_route(
test_data: TestEvent,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create event_notification in the Database """
if test_data.id:
event_obj: EventNotificationIn = db.event_notifications.get(session, test_data.id)
test_data.test_url = event_obj.notification_url
try:
test_notification(test_data.test_url)
except Exception as e:
logger.error(e)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Update event_notification in the Database """
# Update Item
return {"details": "not yet implemented"}
@router.delete("/notifications/{id}")
async def delete_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)

View File

@@ -2,7 +2,7 @@ import operator
import shutil
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi import APIRouter, BackgroundTasks, 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
@@ -33,7 +33,7 @@ def available_imports():
@router.post("/export/database", status_code=status.HTTP_201_CREATED)
def export_database(data: BackupJob, session: Session = Depends(generate_session)):
def export_database(background_tasks: BackgroundTasks, data: BackupJob, session: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
try:
export_path = backup_all(
@@ -47,7 +47,9 @@ 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)
background_tasks.add_task(
create_backup_event, "Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session
)
return {"export_path": export_path}
except Exception as e:
logger.error(e)
@@ -75,7 +77,12 @@ async def download_backup_file(file_name: str):
@router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
def import_database(file_name: str, import_data: ImportJob, session: Session = Depends(generate_session)):
def import_database(
background_tasks: BackgroundTasks,
file_name: str,
import_data: ImportJob,
session: Session = Depends(generate_session),
):
""" Import a database backup file generated from Mealie. """
db_import = imports.import_database(
@@ -90,7 +97,7 @@ 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"Restore File: {file_name}", session)
background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session)
return db_import

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, 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
@@ -32,6 +32,7 @@ async def get_current_user_group(
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_group(
background_tasks: BackgroundTasks,
group_data: GroupBase,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
@@ -40,7 +41,7 @@ async def create_group(
try:
db.groups.create(session, group_data.dict())
create_group_event("Group Created", f"'{group_data.name}' created")
background_tasks.add_task(create_group_event, "Group Created", f"'{group_data.name}' created", session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@@ -58,7 +59,10 @@ async def update_group_data(
@router.delete("/{id}")
async def delete_user_group(
id: int, current_user=Depends(get_current_user), session: Session = Depends(generate_session)
background_tasks: BackgroundTasks,
id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
):
""" Removes a user group from the database """
@@ -73,5 +77,8 @@ async def delete_user_group(
if group.users != []:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GROUP_WITH_USERS")
create_group_event("Group Deleted", f"'{group.name}' Deleted")
background_tasks.add_task(
create_group_event, "Group Deleted", f"'{group.name}' deleted by {current_user.full_name}", session
)
db.groups.delete(session, id)

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, 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
@@ -25,16 +25,22 @@ def get_all_meals(
@router.post("/create", status_code=status.HTTP_201_CREATED)
def create_meal_plan(
data: MealPlanIn, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
background_tasks: BackgroundTasks,
data: MealPlanIn,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Creates a meal plan database entry """
processed_plan = process_meals(session, data)
create_group_event("Meal Plan Created", f"Mealplan Created for '{current_user.group}'")
background_tasks.add_task(
create_group_event, "Meal Plan Created", f"Mealplan Created for '{current_user.group}'", session=session
)
return db.meals.create(session, processed_plan.dict())
@router.put("/{plan_id}")
def update_meal_plan(
background_tasks: BackgroundTasks,
plan_id: str,
meal_plan: MealPlanIn,
session: Session = Depends(generate_session),
@@ -45,13 +51,16 @@ def update_meal_plan(
processed_plan = MealPlanInDB(uid=plan_id, **processed_plan.dict())
try:
db.meals.update(session, plan_id, processed_plan.dict())
create_group_event("Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'")
background_tasks.add_task(
create_group_event, "Meal Plan Updated", f"Mealplan Updated for '{current_user.group}'", session=session
)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@router.delete("/{plan_id}")
def delete_meal_plan(
background_tasks: BackgroundTasks,
plan_id,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
@@ -60,7 +69,9 @@ def delete_meal_plan(
try:
db.meals.delete(session, plan_id)
create_group_event("Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'")
background_tasks.add_task(
create_group_event, "Meal Plan Deleted", f"Mealplan Deleted for '{current_user.group}'", session=session
)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@@ -1,12 +1,14 @@
from shutil import copyfileobj
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, status
from fastapi.datastructures import UploadFile
from mealie.core.config import settings
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, RecipeAsset, RecipeURLIn
from mealie.schema.user import UserInDB
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
@@ -20,6 +22,7 @@ logger = get_logger()
@router.post("/create", status_code=201, response_model=str)
def create_from_json(
background_tasks: BackgroundTasks,
data: Recipe,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
@@ -27,22 +30,36 @@ 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)
background_tasks.add_task(
create_recipe_event,
"Recipe Created (URL)",
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
session=session,
attachment=recipe.image_dir.joinpath("min-original.webp"),
)
return recipe.slug
@router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url(
background_tasks: BackgroundTasks,
url: RecipeURLIn,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
current_user: UserInDB = Depends(get_current_user),
):
""" Takes in a URL and attempts to scrape data and load it into the database """
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)
background_tasks.add_task(
create_recipe_event,
"Recipe Created (URL)",
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
session=session,
attachment=recipe.image_dir.joinpath("min-original.webp"),
)
return recipe.slug
@@ -64,7 +81,6 @@ def update_recipe(
""" Updates a recipe by existing slug and data. """
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
print(recipe.assets)
check_assets(original_slug=recipe_slug, recipe=recipe)
@@ -91,6 +107,7 @@ def patch_recipe(
@router.delete("/{recipe_slug}")
def delete_recipe(
background_tasks: BackgroundTasks,
recipe_slug: str,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
@@ -100,7 +117,12 @@ def delete_recipe(
try:
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)
background_tasks.add_task(
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

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Request, status
from fastapi import APIRouter, BackgroundTasks, Depends, Request, status
from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from mealie.core import security
@@ -15,6 +15,7 @@ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
@router.post("/token/long")
@router.post("/token")
def get_token(
background_tasks: BackgroundTasks,
request: Request,
data: OAuth2PasswordRequestForm = Depends(),
session: Session = Depends(generate_session),
@@ -25,7 +26,9 @@ def get_token(
user = authenticate_user(session, email, password)
if not user:
create_user_event("Failed Login", f"Username: {email}, Source IP: '{request.client.host}'")
background_tasks.add_task(
create_user_event, "Failed Login", f"Username: {email}, Source IP: '{request.client.host}'"
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},

View File

@@ -1,6 +1,6 @@
import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from mealie.core import security
from mealie.core.config import app_dirs, settings
@@ -17,13 +17,16 @@ router = APIRouter(prefix="/api/users", tags=["Users"])
@router.post("", response_model=UserOut, status_code=201)
async def create_user(
background_tasks: BackgroundTasks,
new_user: UserIn,
current_user=Depends(get_current_user),
session: Session = Depends(generate_session),
):
new_user.password = get_password_hash(new_user.password)
create_user_event("User Created", f"Created by {current_user.full_name}", session=session)
background_tasks.add_task(
create_user_event, "User Created", f"Created by {current_user.full_name}", session=session
)
return db.users.create(session, new_user.dict())
@@ -138,6 +141,7 @@ async def update_password(
@router.delete("/{id}")
async def delete_user(
background_tasks: BackgroundTasks,
id: int,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
@@ -150,6 +154,6 @@ async def delete_user(
if current_user.id == id or current_user.admin:
try:
db.users.delete(session, id)
create_user_event("User Deleted", f"User ID: {id}", session=session)
background_tasks.add_task(create_user_event, "User Deleted", f"User ID: {id}", session=session)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)

View File

@@ -1,6 +1,6 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, 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
@@ -25,6 +25,7 @@ async def get_all_open_sign_ups(
@router.post("", response_model=SignUpToken)
async def create_user_sign_up_key(
background_tasks: BackgroundTasks,
key_data: SignUpIn,
current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session),
@@ -39,12 +40,16 @@ async def create_user_sign_up_key(
"name": key_data.name,
"admin": key_data.admin,
}
create_user_event("Sign-up Token Created", f"Created by {current_user.full_name}", session=session)
background_tasks.add_task(
create_user_event, "Sign-up Token Created", f"Created by {current_user.full_name}", session=session
)
return db.sign_ups.create(session, sign_up)
@router.post("/{token}")
async def create_user_with_token(
background_tasks: BackgroundTasks,
token: str,
new_user: UserIn,
session: Session = Depends(generate_session),
@@ -62,7 +67,9 @@ async def create_user_with_token(
db.users.create(session, new_user.dict())
# DeleteToken
create_user_event("Sign-up Token Used", f"New User {new_user.full_name}", session=session)
background_tasks.add_task(
create_user_event, "Sign-up Token Used", f"New User {new_user.full_name}", session=session
)
db.sign_ups.delete(session, token)