mirror of
https://github.com/markbeep/AudioBookRequest.git
synced 2026-01-07 06:00:04 -06:00
Merge pull request #88 from markbeep/15-allow-editing-notifications
allow editing notifications
This commit is contained in:
@@ -57,6 +57,8 @@ async def send_all_notifications(
|
||||
select(Notification).where(Notification.event == event_type)
|
||||
).all()
|
||||
for notification in notifications:
|
||||
if not notification.enabled:
|
||||
continue
|
||||
await send_notification(
|
||||
session=session,
|
||||
notification=notification,
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.internal.auth.authentication import (
|
||||
raise_for_invalid_password,
|
||||
)
|
||||
from app.internal.auth.config import LoginTypeEnum, auth_config
|
||||
from app.internal.env_settings import Settings
|
||||
from app.internal.models import GroupEnum
|
||||
from app.util.db import get_session
|
||||
from app.util.templates import templates
|
||||
@@ -30,7 +31,7 @@ etag_cache: dict[PathLike[str] | str, str] = {}
|
||||
def add_cache_headers(func: Callable[..., FileResponse]):
|
||||
def wrapper(v: str):
|
||||
file = func()
|
||||
if not (etag := etag_cache.get(file.path)):
|
||||
if not (etag := etag_cache.get(file.path)) or Settings().app.debug:
|
||||
with open(file.path, "rb") as f:
|
||||
etag = hashlib.sha1(f.read(), usedforsecurity=False).hexdigest()
|
||||
etag_cache[file.path] = etag
|
||||
|
||||
@@ -461,6 +461,80 @@ def read_notifications(
|
||||
)
|
||||
|
||||
|
||||
def _list_notifications(request: Request, session: Session, admin_user: DetailedUser):
|
||||
notifications = session.exec(select(Notification)).all()
|
||||
event_types = [e.value for e in EventEnum]
|
||||
notifications = session.exec(select(Notification)).all()
|
||||
event_types = [e.value for e in EventEnum]
|
||||
return template_response(
|
||||
"settings_page/notifications.html",
|
||||
request,
|
||||
admin_user,
|
||||
{
|
||||
"page": "notifications",
|
||||
"notifications": notifications,
|
||||
"event_types": event_types,
|
||||
},
|
||||
block_name="notfications_block",
|
||||
)
|
||||
|
||||
|
||||
def _upsert_notification(
|
||||
request: Request,
|
||||
name: str,
|
||||
apprise_url: str,
|
||||
title_template: str,
|
||||
body_template: str,
|
||||
event_type: str,
|
||||
headers: str,
|
||||
admin_user: DetailedUser,
|
||||
session: Session,
|
||||
notification_id: Optional[uuid.UUID] = None,
|
||||
):
|
||||
if not headers:
|
||||
headers = "{}"
|
||||
try:
|
||||
headers_json = json.loads(headers)
|
||||
if not isinstance(headers_json, dict) or any(
|
||||
not isinstance(v, str) for v in cast(dict[str, Any], headers_json).values()
|
||||
):
|
||||
raise ValueError()
|
||||
headers_json = cast(dict[str, str], headers_json)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
raise ToastException("Invalid headers JSON", "error")
|
||||
|
||||
try:
|
||||
event_enum = EventEnum(event_type)
|
||||
except ValueError:
|
||||
raise ToastException("Invalid event type", "error")
|
||||
|
||||
if notification_id:
|
||||
notification = session.get(Notification, notification_id)
|
||||
if not notification:
|
||||
raise ToastException("Notification not found", "error")
|
||||
notification.name = name
|
||||
notification.apprise_url = apprise_url
|
||||
notification.event = event_enum
|
||||
notification.title_template = title_template
|
||||
notification.body_template = body_template
|
||||
notification.headers = headers_json
|
||||
notification.enabled = True
|
||||
else:
|
||||
notification = Notification(
|
||||
name=name,
|
||||
apprise_url=apprise_url,
|
||||
event=event_enum,
|
||||
title_template=title_template,
|
||||
body_template=body_template,
|
||||
headers=headers_json,
|
||||
enabled=True,
|
||||
)
|
||||
session.add(notification)
|
||||
session.commit()
|
||||
|
||||
return _list_notifications(request, session, admin_user)
|
||||
|
||||
|
||||
@router.post("/notification")
|
||||
def add_notification(
|
||||
request: Request,
|
||||
@@ -475,57 +549,65 @@ def add_notification(
|
||||
],
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
):
|
||||
if not headers:
|
||||
headers = "{}"
|
||||
try:
|
||||
headers_json = json.loads(headers)
|
||||
if not isinstance(headers_json, dict) or any(
|
||||
not isinstance(v, str) for v in cast(dict[str, Any], headers_json).values()
|
||||
):
|
||||
raise ValueError()
|
||||
headers_json = cast(dict[str, str], headers_json)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return template_response(
|
||||
"settings_page/notifications.html",
|
||||
request,
|
||||
admin_user,
|
||||
{"page": "notifications", "error": "Invalid headers JSON"},
|
||||
block_name="form_error",
|
||||
)
|
||||
|
||||
try:
|
||||
event_enum = EventEnum(event_type)
|
||||
except ValueError:
|
||||
return template_response(
|
||||
"settings_page/notifications.html",
|
||||
request,
|
||||
admin_user,
|
||||
{"page": "notifications", "error": "Invalid event type"},
|
||||
block_name="form_error",
|
||||
)
|
||||
|
||||
notification = Notification(
|
||||
return _upsert_notification(
|
||||
request=request,
|
||||
name=name,
|
||||
apprise_url=apprise_url,
|
||||
event=event_enum,
|
||||
title_template=title_template,
|
||||
body_template=body_template,
|
||||
headers=headers_json,
|
||||
enabled=True,
|
||||
event_type=event_type,
|
||||
headers=headers,
|
||||
admin_user=admin_user,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/notification/{notification_id}")
|
||||
def update_notification(
|
||||
request: Request,
|
||||
notification_id: uuid.UUID,
|
||||
name: Annotated[str, Form()],
|
||||
apprise_url: Annotated[str, Form()],
|
||||
title_template: Annotated[str, Form()],
|
||||
body_template: Annotated[str, Form()],
|
||||
event_type: Annotated[str, Form()],
|
||||
headers: Annotated[str, Form()],
|
||||
admin_user: Annotated[
|
||||
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
|
||||
],
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
):
|
||||
return _upsert_notification(
|
||||
request=request,
|
||||
name=name,
|
||||
apprise_url=apprise_url,
|
||||
title_template=title_template,
|
||||
body_template=body_template,
|
||||
event_type=event_type,
|
||||
headers=headers,
|
||||
admin_user=admin_user,
|
||||
session=session,
|
||||
notification_id=notification_id,
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/notification/{notification_id}/enable")
|
||||
def toggle_notification(
|
||||
request: Request,
|
||||
notification_id: uuid.UUID,
|
||||
admin_user: Annotated[
|
||||
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
|
||||
],
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
):
|
||||
notification = session.get_one(Notification, notification_id)
|
||||
if not notification:
|
||||
raise ToastException("Notification not found", "error")
|
||||
notification.enabled = not notification.enabled
|
||||
session.add(notification)
|
||||
session.commit()
|
||||
|
||||
notifications = session.exec(select(Notification)).all()
|
||||
|
||||
return template_response(
|
||||
"settings_page/notifications.html",
|
||||
request,
|
||||
admin_user,
|
||||
{"page": "notifications", "notifications": notifications},
|
||||
block_name="notfications_block",
|
||||
headers={"HX-Retarget": "#notification-list"},
|
||||
)
|
||||
return _list_notifications(request, session, admin_user)
|
||||
|
||||
|
||||
@router.delete("/notification/{notification_id}")
|
||||
@@ -537,25 +619,17 @@ def delete_notification(
|
||||
],
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
):
|
||||
notifications = session.exec(select(Notification)).all()
|
||||
for notif in notifications:
|
||||
if notif.id == notification_id:
|
||||
session.delete(notif)
|
||||
session.commit()
|
||||
break
|
||||
notifications = session.exec(select(Notification)).all()
|
||||
notification = session.get_one(Notification, notification_id)
|
||||
if not notification:
|
||||
raise ToastException("Notification not found", "error")
|
||||
session.delete(notification)
|
||||
session.commit()
|
||||
|
||||
return template_response(
|
||||
"settings_page/notifications.html",
|
||||
request,
|
||||
admin_user,
|
||||
{"page": "notifications", "notifications": notifications},
|
||||
block_name="notfications_block",
|
||||
)
|
||||
return _list_notifications(request, session, admin_user)
|
||||
|
||||
|
||||
@router.post("/notification/{notification_id}")
|
||||
async def execute_notification(
|
||||
async def test_notification(
|
||||
notification_id: uuid.UUID,
|
||||
admin_user: Annotated[
|
||||
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
|
||||
|
||||
@@ -12,6 +12,9 @@ templates.env.filters["zfill"] = lambda val, num: str(val).zfill(num) # pyright
|
||||
templates.env.globals["vars"] = vars # pyright: ignore[reportUnknownMemberType]
|
||||
templates.env.globals["getattr"] = getattr # pyright: ignore[reportUnknownMemberType]
|
||||
templates.env.globals["version"] = Settings().app.version # pyright: ignore[reportUnknownMemberType]
|
||||
templates.env.globals["json_regexp"] = ( # pyright: ignore[reportUnknownMemberType]
|
||||
r'^\{\s*(?:"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*(?:,\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*)*)?\}$'
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
|
||||
19
templates/icons/pencil.html
Normal file
19
templates/icons/pencil.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke-width="2"
|
||||
style="--darkreader-inline-stroke: currentColor"
|
||||
data-darkreader-inline-stroke=""
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 534 B |
@@ -1,6 +1,6 @@
|
||||
{% extends "settings_page/base.html" %} {% block head %}
|
||||
<title>Settings - Notifications</title>
|
||||
{% endblock %} {% block content %}
|
||||
{% include 'scripts/alpinejs.html' %} {% endblock %} {% block content %}
|
||||
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg">Notifications</h2>
|
||||
@@ -9,15 +9,11 @@
|
||||
id="add-notification-form"
|
||||
class="flex flex-col gap-2"
|
||||
hx-post="/settings/notification"
|
||||
hx-target="#error-message"
|
||||
hx-target="#notification-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if (event.detail.successful) this.reset()"
|
||||
hx-on::after-request="if (event.detail.successful && event.detail.target?.id === 'notification-list') this.reset()"
|
||||
>
|
||||
{% block form_error %}
|
||||
<span id="error-message" class="text-red-400">{{ error }}</span>
|
||||
{% endblock %}
|
||||
|
||||
<label for="name">Name</label>
|
||||
<label for="name">Name<span class="text-error">*</span></label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
@@ -27,7 +23,7 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="event">Event</label>
|
||||
<label for="event">Event<span class="text-error">*</span></label>
|
||||
<select id="event" name="event_type" class="select w-full" required>
|
||||
{% for e in event_types %}
|
||||
<option value="{{ e }}">{{ e }}</option>
|
||||
@@ -36,9 +32,8 @@
|
||||
|
||||
<label for="apprise_url"
|
||||
>Apprise Notify URL
|
||||
<span class="text-xs font-mono"
|
||||
>(http://.../notify/c2h3fg...)</span
|
||||
></label
|
||||
<span class="text-xs font-mono">(http://.../notify/c2h3fg...)</span
|
||||
><span class="text-error">*</span></label
|
||||
>
|
||||
<input
|
||||
id="apprise_url"
|
||||
@@ -50,7 +45,8 @@
|
||||
/>
|
||||
|
||||
<label for="headers"
|
||||
>Headers <span class="astext-xs font-mono">(JSON format)</span></label
|
||||
>Headers
|
||||
<span class="astext-xs font-mono">(JSON format, optional)</span></label
|
||||
>
|
||||
<!-- prettier-ignore -->
|
||||
<input
|
||||
@@ -59,10 +55,12 @@
|
||||
type="text"
|
||||
class="input w-full"
|
||||
placeholder="{"username": "admin", "password": "password123"}"
|
||||
pattern="{{'^\{\s*(?:"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*(?:,\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*"[^"\\]*(?:\\.[^"\\]*)*"\s*)*)?\}$'}}"
|
||||
pattern="{{ json_regexp }}"
|
||||
/>
|
||||
|
||||
<label for="title_template">Title Template</label>
|
||||
<label for="title_template"
|
||||
>Title Template<span class="text-error">*</span></label
|
||||
>
|
||||
<input
|
||||
id="title_template"
|
||||
placeholder="New Book: {bookTitle}"
|
||||
@@ -73,7 +71,9 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="body_template">Body Template</label>
|
||||
<label for="body_template"
|
||||
>Body Template<span class="text-error">*</span></label
|
||||
>
|
||||
<textarea
|
||||
id="body_template"
|
||||
placeholder="New book {bookTitle} by {bookAuthors} narrated by {bookNarrators}. `Requested by {eventUser}`"
|
||||
@@ -96,7 +96,14 @@
|
||||
</div>
|
||||
|
||||
{% block notfications_block %}
|
||||
<div id="notification-list" class="pt-4 border-t border-base-200">
|
||||
<div
|
||||
id="notification-list"
|
||||
class="pt-4 border-t border-base-200"
|
||||
x-data="{ edit: null }"
|
||||
x-init="$watch('edit', value => { // load htmx when the form is shown
|
||||
if (edit) htmx.process(document.querySelector('#edit-notification-form'))
|
||||
})"
|
||||
>
|
||||
<h2 class="text-lg">Apprise Notifications</h2>
|
||||
|
||||
<div class="max-h-[30rem] overflow-x-auto">
|
||||
@@ -117,31 +124,144 @@
|
||||
<td>{{ n.name }}</td>
|
||||
<td>{{ n.event.value }}</td>
|
||||
<td>{{ n.apprise_url }}</td>
|
||||
<td class="flex gap-1">
|
||||
<td class="grid grid-cols-2 min-w-[8rem] gap-1">
|
||||
<button
|
||||
title="Delete notification"
|
||||
class="btn btn-error btn-square"
|
||||
hx-delete="/settings/notification/{{n.id}}"
|
||||
hx-target="#notification-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this notification?"
|
||||
>
|
||||
{% include 'icons/trash.html' %}
|
||||
</button>
|
||||
<button
|
||||
title="Test notification"
|
||||
title="Test"
|
||||
class="btn btn-square"
|
||||
hx-post="/settings/notification/{{n.id}}"
|
||||
hx-disabled-elt="this"
|
||||
>
|
||||
{% include 'icons/test-pipe.html' %}
|
||||
</button>
|
||||
<button
|
||||
title="Edit"
|
||||
class="btn btn-square"
|
||||
x-on:click="edit === '{{n.id}}' ? edit=null: edit='{{n.id}}'"
|
||||
>
|
||||
{% include 'icons/pencil.html' %}
|
||||
</button>
|
||||
<button
|
||||
title="{{ 'Enabled' if n.enabled else 'Disabled' }}"
|
||||
class="btn btn-square {{ 'btn-success' if n.enabled else 'btn-error' }}"
|
||||
hx-patch="/settings/notification/{{n.id}}/enable"
|
||||
hx-disabled-elt="this"
|
||||
hx-target="#notification-list"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% if n.enabled %}
|
||||
<span>{% include 'icons/checkmark.html' %}</span> {% else %}
|
||||
<span>{% include 'icons/xmark.html' %}</span> {% endif %}
|
||||
</button>
|
||||
<button
|
||||
title="Delete"
|
||||
class="btn btn-error btn-square"
|
||||
hx-delete="/settings/notification/{{n.id}}"
|
||||
hx-target="#notification-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to delete this notification? ({{n.name}})"
|
||||
>
|
||||
{% include 'icons/trash.html' %}
|
||||
</button>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% for n in notifications %}
|
||||
<template x-if="edit === '{{n.id}}'">
|
||||
<form
|
||||
x-data="{ name: '{{n.name}}', event: '{{n.event.value}}', apprise_url: '{{n.apprise_url}}', headers: '{{n.headers}}', title_template: '{{n.title_template}}', body_template: '{{n.body_template}}' }"
|
||||
class="flex flex-col gap-2"
|
||||
hx-put="/settings/notification/{{n.id}}"
|
||||
hx-target="#notification-list"
|
||||
hx-swap="outerHTML"
|
||||
id="edit-notification-form"
|
||||
>
|
||||
<label for="name">Name<span class="text-error">*</span></label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
minlength="1"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
required
|
||||
x-model="name"
|
||||
/>
|
||||
|
||||
<label for="event">Event<span class="text-error">*</span></label>
|
||||
<select
|
||||
id="event"
|
||||
name="event_type"
|
||||
class="select w-full"
|
||||
required
|
||||
x-model="event"
|
||||
>
|
||||
{% for e in event_types %}
|
||||
<option value="{{ e }}">{{ e }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="apprise_url"
|
||||
>Apprise Notify URL
|
||||
<span class="text-xs font-mono">(http://.../notify/c2h3fg...)</span
|
||||
><span class="text-error">*</span></label
|
||||
>
|
||||
<input
|
||||
id="apprise_url"
|
||||
name="apprise_url"
|
||||
minlength="1"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
required
|
||||
x-model="apprise_url"
|
||||
/>
|
||||
|
||||
<label for="headers"
|
||||
>Headers
|
||||
<span class="astext-xs font-mono">(JSON format, optional)</span></label
|
||||
>
|
||||
<!-- prettier-ignore -->
|
||||
<input
|
||||
id="headers"
|
||||
name="headers"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
placeholder="{"username": "admin", "password": "password123"}"
|
||||
pattern="{{ json_regexp }}"
|
||||
x-model="headers"
|
||||
/>
|
||||
|
||||
<label for="title_template"
|
||||
>Title Template<span class="text-error">*</span></label
|
||||
>
|
||||
<input
|
||||
id="title_template"
|
||||
placeholder="New Book: {bookTitle}"
|
||||
name="title_template"
|
||||
minlength="1"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
required
|
||||
x-model="title_template"
|
||||
/>
|
||||
|
||||
<label for="body_template"
|
||||
>Body Template<span class="text-error">*</span></label
|
||||
>
|
||||
<textarea
|
||||
id="body_template"
|
||||
placeholder="New book {bookTitle} by {bookAuthors} narrated by {bookNarrators}. `Requested by {eventUser}`"
|
||||
name="body_template"
|
||||
class="textarea w-full"
|
||||
required
|
||||
x-model="body_template"
|
||||
></textarea>
|
||||
|
||||
<button type="submit" class="btn">Update</button>
|
||||
</form>
|
||||
</template>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %} {% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user