Feature/group based notifications (#918)

* fix group page

* setup group notification for backend

* update type generators

* script to auto-generate schema exports

* setup frontend CRUD interface

* remove old notifications UI

* drop old events api

* add test functionality

* update naming for fields

* add event dispatcher functionality

* bump to python 3.10

* bump python version

* purge old event code

* use-async apprise

* set mealie logo as image

* unify styles for buttons rows

* add links to banners
This commit is contained in:
Hayden
2022-01-09 21:04:24 -09:00
committed by GitHub
parent 50a341ed3f
commit 190773c5d7
74 changed files with 1992 additions and 1229 deletions

View File

@@ -76,6 +76,17 @@ class CrudMixins:
return item
def get_one(self, item_id):
item = self.repo.get(item_id)
if not item:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail=ErrorResponse.respond(message="Not found."),
)
return item
def update_one(self, data, item_id):
item = self.repo.get(item_id)
@@ -98,11 +109,11 @@ class CrudMixins:
self.handle_exception(ex)
def delete_one(self, item_id):
item = self.repo.get(item_id)
self.logger.info(f"Deleting item with id {item}")
self.logger.info(f"Deleting item with id {item_id}")
try:
item = self.repo.delete(item)
item = self.repo.delete(item_id)
self.logger.info(item)
except Exception as ex:
self.handle_exception(ex)

View File

@@ -1,8 +1,7 @@
from fastapi import APIRouter
from . import events, notifications
from . import events
about_router = APIRouter(prefix="/api/about")
about_router.include_router(events.router, tags=["Events: CRUD"])
about_router.include_router(notifications.router, tags=["Events: Notifications"])

View File

@@ -1,67 +0,0 @@
from http.client import HTTPException
from fastapi import Depends, status
from sqlalchemy.orm.session import Session
from mealie.core.root_logger import get_logger
from mealie.db.db_setup import generate_session
from mealie.repos.all_repositories import get_repositories
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.events import EventNotificationIn, EventNotificationOut, TestEvent
from mealie.services.events import test_notification
router = AdminAPIRouter()
logger = get_logger()
@router.post("/notifications")
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
):
"""Create event_notification in the Database"""
db = get_repositories(session)
return db.event_notifications.create(event_data)
@router.post("/notifications/test")
async def test_notification_route(
test_data: TestEvent,
session: Session = Depends(generate_session),
):
"""Create event_notification in the Database"""
db = get_repositories(session)
if test_data.id:
event_obj: EventNotificationIn = db.event_notifications.get(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)):
"""Get all event_notification from the Database"""
db = get_repositories(session)
return db.event_notifications.get_all(override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(id: int, session: Session = Depends(generate_session)):
"""Update event_notification in the Database"""
# not yet implemented
raise HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED)
@router.delete("/notifications/{id}")
async def delete_event_notification(id: int, session: Session = Depends(generate_session)):
"""Delete event_notification from the Database"""
# Delete Item
db = get_repositories(session)
return db.event_notifications.delete(id)

View File

@@ -8,7 +8,7 @@ from mealie.services.group_services import CookbookService, WebhookService
from mealie.services.group_services.meal_service import MealService
from mealie.services.group_services.reports_service import GroupReportService
from . import categories, invitations, labels, migrations, preferences, self_service, shopping_lists
from . import categories, invitations, labels, migrations, notifications, preferences, self_service, shopping_lists
router = APIRouter()
@@ -56,3 +56,4 @@ def get_all_reports(
router.include_router(report_router)
router.include_router(shopping_lists.router)
router.include_router(labels.router)
router.include_router(notifications.router)

View File

@@ -0,0 +1,85 @@
from functools import cached_property
from sqlite3 import IntegrityError
from typing import Type
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.controller import controller
from mealie.routes._base.dependencies import SharedDependencies
from mealie.routes._base.mixins import CrudMixins
from mealie.schema.group.group_events import (
GroupEventNotifierCreate,
GroupEventNotifierOut,
GroupEventNotifierPrivate,
GroupEventNotifierSave,
GroupEventNotifierUpdate,
)
from mealie.schema.mapper import cast
from mealie.schema.query import GetAll
from mealie.services.event_bus_service.event_bus_service import EventBusService
router = APIRouter(prefix="/groups/events/notifications", tags=["Group: Event Notifications"])
@controller(router)
class GroupEventsNotifierController:
deps: SharedDependencies = Depends(SharedDependencies.user)
event_bus: EventBusService = Depends(EventBusService)
@cached_property
def repo(self):
if not self.deps.acting_user:
raise Exception("No user is logged in.")
return self.deps.repos.group_event_notifier.by_group(self.deps.acting_user.group_id)
def registered_exceptions(self, ex: Type[Exception]) -> str:
registered = {
Exception: "An unexpected error occurred.",
IntegrityError: "An unexpected error occurred.",
}
return registered.get(ex, "An unexpected error occurred.")
# =======================================================================
# CRUD Operations
@property
def mixins(self) -> CrudMixins:
return CrudMixins(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
@router.get("", response_model=list[GroupEventNotifierOut])
def get_all(self, q: GetAll = Depends(GetAll)):
return self.repo.get_all(start=q.start, limit=q.limit)
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
def create_one(self, data: GroupEventNotifierCreate):
save_data = cast(data, GroupEventNotifierSave, group_id=self.deps.acting_user.group_id)
return self.mixins.create_one(save_data)
@router.get("/{item_id}", response_model=GroupEventNotifierOut)
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=GroupEventNotifierOut)
def update_one(self, item_id: UUID4, data: GroupEventNotifierUpdate):
if data.apprise_url is None:
current_data: GroupEventNotifierPrivate = self.repo.get_one(
item_id, override_schema=GroupEventNotifierPrivate
)
data.apprise_url = current_data.apprise_url
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", status_code=204)
def delete_one(self, item_id: UUID4):
self.mixins.delete_one(item_id) # type: ignore
# =======================================================================
# Test Event Notifications
@router.post("/{item_id}/test", status_code=204)
def test_notification(self, item_id: UUID4):
item: GroupEventNotifierPrivate = self.repo.get_one(item_id, override_schema=GroupEventNotifierPrivate)
self.event_bus.test_publisher(item.apprise_url)

View File

@@ -17,6 +17,8 @@ from mealie.schema.group.group_shopping_list import (
)
from mealie.schema.mapper import cast
from mealie.schema.query import GetAll
from mealie.services.event_bus_service.event_bus_service import EventBusService
from mealie.services.event_bus_service.message_types import EventTypes
from mealie.services.group_services.shopping_lists import ShoppingListService
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"])
@@ -26,6 +28,7 @@ router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists
class ShoppingListRoutes:
deps: SharedDependencies = Depends(SharedDependencies.user)
service: ShoppingListService = Depends(ShoppingListService.private)
event_bus: EventBusService = Depends(EventBusService)
@cached_property
def repo(self):
@@ -56,7 +59,16 @@ class ShoppingListRoutes:
@router.post("", response_model=ShoppingListOut)
def create_one(self, data: ShoppingListCreate):
save_data = cast(data, ShoppingListSave, group_id=self.deps.acting_user.group_id)
return self.mixins.create_one(save_data)
val = self.mixins.create_one(save_data)
if val:
self.event_bus.dispatch(
self.deps.acting_user.group_id,
EventTypes.shopping_list_created,
msg="A new shopping list has been created.",
)
return val
@router.get("/{item_id}", response_model=ShoppingListOut)
def get_one(self, item_id: UUID4):