Add test notification button (#996)
* Add api to test notifications before you save * Add button to UI * Inspect apprise logs to get errors * Better formatting around errors coming back from the test notification endpoint * Use apprise's built in log capture * Instruct the user to get error from container log instead of intercepting and parsing apprise logs * refac move to test method on notification class --------- Co-authored-by: Simon <simobilleter@gmail.com>
This commit is contained in:
parent
624a5f9bd4
commit
aefd678dca
|
|
@ -329,3 +329,21 @@ SPECTACULAR_SETTINGS = {
|
||||||
"VERSION": TA_VERSION,
|
"VERSION": TA_VERSION,
|
||||||
"SERVE_INCLUDE_SCHEMA": False,
|
"SERVE_INCLUDE_SCHEMA": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"apprise": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,3 +91,12 @@ class TaskNotificationPostSerializer(serializers.Serializer):
|
||||||
|
|
||||||
task_name = serializers.ChoiceField(choices=list(TASK_CONFIG))
|
task_name = serializers.ChoiceField(choices=list(TASK_CONFIG))
|
||||||
url = serializers.CharField(required=False)
|
url = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskNotificationTestSerializer(serializers.Serializer):
|
||||||
|
"""serialize task notification test POST"""
|
||||||
|
|
||||||
|
url = serializers.CharField()
|
||||||
|
task_name = serializers.ChoiceField(
|
||||||
|
choices=list(TASK_CONFIG), required=False
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,38 @@ class Notifications:
|
||||||
|
|
||||||
apobj.notify(body=body, title=title)
|
apobj.notify(body=body, title=title)
|
||||||
|
|
||||||
|
def test(self, url) -> tuple[bool, str]:
|
||||||
|
"""send test notification"""
|
||||||
|
try:
|
||||||
|
apobj = apprise.Apprise()
|
||||||
|
|
||||||
|
if not apobj.add(url):
|
||||||
|
success = False
|
||||||
|
message = f"Invalid notification URL format: {url}"
|
||||||
|
return success, message
|
||||||
|
|
||||||
|
title = f"[TA] {self.task_name} process ended with SUCCESS"
|
||||||
|
body = "This is a test notification. Task completed successfully."
|
||||||
|
|
||||||
|
result = apobj.notify(body=body, title=title)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
success = True
|
||||||
|
message = "Test notification sent successfully"
|
||||||
|
return success, message
|
||||||
|
|
||||||
|
success = False
|
||||||
|
message = (
|
||||||
|
"Notification failed. "
|
||||||
|
"Please check container logs for more information."
|
||||||
|
)
|
||||||
|
return success, message
|
||||||
|
|
||||||
|
except Exception as err: # pylint: disable=broad-exception-caught
|
||||||
|
success = False
|
||||||
|
message = f"Notification error: {str(err)}"
|
||||||
|
return success, message
|
||||||
|
|
||||||
def _build_message(
|
def _build_message(
|
||||||
self, task_id: str, task_title: str
|
self, task_id: str, task_title: str
|
||||||
) -> tuple[str, str | None]:
|
) -> tuple[str, str | None]:
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,9 @@ urlpatterns = [
|
||||||
views.ScheduleNotification.as_view(),
|
views.ScheduleNotification.as_view(),
|
||||||
name="api-schedule-notification",
|
name="api-schedule-notification",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"notification/test/",
|
||||||
|
views.NotificationTestView.as_view(),
|
||||||
|
name="api-schedule-notification-test",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from task.serializers import (
|
||||||
TaskIDDataSerializer,
|
TaskIDDataSerializer,
|
||||||
TaskNotificationPostSerializer,
|
TaskNotificationPostSerializer,
|
||||||
TaskNotificationSerializer,
|
TaskNotificationSerializer,
|
||||||
|
TaskNotificationTestSerializer,
|
||||||
TaskResultSerializer,
|
TaskResultSerializer,
|
||||||
)
|
)
|
||||||
from task.src.config_schedule import CrontabValidator, ScheduleBuilder
|
from task.src.config_schedule import CrontabValidator, ScheduleBuilder
|
||||||
|
|
@ -343,3 +344,34 @@ class ScheduleNotification(ApiBaseView):
|
||||||
Notifications(task_name).remove_task()
|
Notifications(task_name).remove_task()
|
||||||
|
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationTestView(ApiBaseView):
|
||||||
|
"""resolves to /api/task/notification/test/
|
||||||
|
POST: test notification url
|
||||||
|
"""
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=TaskNotificationTestSerializer(),
|
||||||
|
responses={
|
||||||
|
200: OpenApiResponse(description="test notification sent"),
|
||||||
|
400: OpenApiResponse(
|
||||||
|
ErrorResponseSerializer(), description="bad request"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
"""test notification"""
|
||||||
|
data_serializer = TaskNotificationTestSerializer(data=request.data)
|
||||||
|
data_serializer.is_valid(raise_exception=True)
|
||||||
|
validated_data = data_serializer.validated_data
|
||||||
|
|
||||||
|
url = validated_data["url"]
|
||||||
|
task_name = validated_data.get("task_name", "manual_test")
|
||||||
|
|
||||||
|
success, message = Notifications(task_name).test(url)
|
||||||
|
|
||||||
|
status = 200 if success else 400
|
||||||
|
return Response(
|
||||||
|
{"success": success, "message": message}, status=status
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import APIClient from '../../functions/APIClient';
|
||||||
|
import { AppriseTaskNameType } from './createAppriseNotificationUrl';
|
||||||
|
|
||||||
|
export type TestNotificationResponseType = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAppriseNotificationUrl = async (taskName: AppriseTaskNameType, url: string) => {
|
||||||
|
return APIClient<TestNotificationResponseType>('/api/task/notification/test/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { task_name: taskName, url },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default testAppriseNotificationUrl;
|
||||||
|
|
@ -9,6 +9,7 @@ export interface ButtonProps {
|
||||||
children?: string | ReactNode | ReactNode[];
|
children?: string | ReactNode | ReactNode[];
|
||||||
value?: string;
|
value?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
disabled?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ const Button = ({
|
||||||
children,
|
children,
|
||||||
value,
|
value,
|
||||||
title,
|
title,
|
||||||
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
}: ButtonProps) => {
|
}: ButtonProps) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,6 +33,7 @@ const Button = ({
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
title={title}
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import createTaskSchedule from '../api/actions/createTaskSchedule';
|
||||||
import createAppriseNotificationUrl, {
|
import createAppriseNotificationUrl, {
|
||||||
AppriseTaskNameType,
|
AppriseTaskNameType,
|
||||||
} from '../api/actions/createAppriseNotificationUrl';
|
} from '../api/actions/createAppriseNotificationUrl';
|
||||||
|
import testAppriseNotificationUrl, {
|
||||||
|
TestNotificationResponseType,
|
||||||
|
} from '../api/actions/testAppriseNotificationUrl';
|
||||||
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
||||||
import { ApiError, ApiResponseType } from '../functions/APIClient';
|
import { ApiError, ApiResponseType } from '../functions/APIClient';
|
||||||
|
|
||||||
|
|
@ -62,6 +65,10 @@ const SettingsScheduling = () => {
|
||||||
const [downloadPendingError, setDownloadPendingError] = useState<string | null>(null);
|
const [downloadPendingError, setDownloadPendingError] = useState<string | null>(null);
|
||||||
const [thumnailCheckError, setThumnailCheckError] = useState<string | null>(null);
|
const [thumnailCheckError, setThumnailCheckError] = useState<string | null>(null);
|
||||||
const [zipBackupError, setZipBackupError] = useState<string | null>(null);
|
const [zipBackupError, setZipBackupError] = useState<string | null>(null);
|
||||||
|
const [testNotificationMessage, setTestNotificationMessage] = useState<{
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const { data: appriseNotificationData } = appriseNotification ?? {};
|
const { data: appriseNotificationData } = appriseNotification ?? {};
|
||||||
|
|
||||||
|
|
@ -539,49 +546,99 @@ const SettingsScheduling = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-item">
|
<div className="settings-item">
|
||||||
<p>
|
<div>
|
||||||
<i>
|
<p>
|
||||||
Send notification on completed tasks with the help of the{' '}
|
<i>
|
||||||
<a href="https://github.com/caronc/apprise" target="_blank">
|
Send notification on completed tasks with the help of the{' '}
|
||||||
Apprise
|
<a href="https://github.com/caronc/apprise" target="_blank">
|
||||||
</a>{' '}
|
Apprise
|
||||||
library.
|
</a>{' '}
|
||||||
</i>
|
library.
|
||||||
</p>
|
</i>
|
||||||
|
</p>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={notificationTask}
|
value={notificationTask}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setNotificationTask(e.currentTarget.value);
|
setNotificationTask(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">-- select task --</option>
|
<option value="">-- select task --</option>
|
||||||
<option value="update_subscribed">Rescan your Subscriptions</option>
|
<option value="update_subscribed">Rescan your Subscriptions</option>
|
||||||
<option value="extract_download">Add to download queue</option>
|
<option value="extract_download">Add to download queue</option>
|
||||||
<option value="download_pending">Downloading</option>
|
<option value="download_pending">Downloading</option>
|
||||||
<option value="check_reindex">Reindex Documents</option>
|
<option value="check_reindex">Reindex Documents</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Apprise notification URL"
|
placeholder="Apprise notification URL"
|
||||||
value={notificationUrl}
|
value={notificationUrl}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setNotificationUrl(e.currentTarget.value);
|
setNotificationUrl(e.currentTarget.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<div className="button-box">
|
||||||
label="Save"
|
<Button
|
||||||
onClick={async () => {
|
label="Save"
|
||||||
await createAppriseNotificationUrl(
|
disabled={!notificationUrl || !notificationTask}
|
||||||
notificationTask as AppriseTaskNameType,
|
onClick={async () => {
|
||||||
notificationUrl || '',
|
await createAppriseNotificationUrl(
|
||||||
);
|
notificationTask as AppriseTaskNameType,
|
||||||
|
notificationUrl || '',
|
||||||
|
);
|
||||||
|
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label="Test"
|
||||||
|
disabled={!notificationUrl || !notificationTask}
|
||||||
|
onClick={async () => {
|
||||||
|
setTestNotificationMessage(null);
|
||||||
|
try {
|
||||||
|
const response: ApiResponseType<TestNotificationResponseType> =
|
||||||
|
await testAppriseNotificationUrl(
|
||||||
|
notificationTask as AppriseTaskNameType,
|
||||||
|
notificationUrl || '',
|
||||||
|
);
|
||||||
|
if (response.data?.success) {
|
||||||
|
setTestNotificationMessage({
|
||||||
|
message: response.data.message,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTestNotificationMessage({
|
||||||
|
message: response.data?.message || 'Test notification failed',
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const apiError = error as ApiError;
|
||||||
|
if (apiError.status && apiError.message) {
|
||||||
|
setTestNotificationMessage({ message: apiError.message, success: false });
|
||||||
|
} else {
|
||||||
|
setTestNotificationMessage({
|
||||||
|
message: 'An unexpected error occurred while testing notification.',
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRefresh(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{testNotificationMessage && (
|
||||||
|
<div
|
||||||
|
className={!testNotificationMessage.success ? 'danger-zone' : ''}
|
||||||
|
style={{ margin: '10px 5px' }}
|
||||||
|
>
|
||||||
|
<p>{testNotificationMessage.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,17 @@ button:hover {
|
||||||
color: var(--main-bg);
|
color: var(--main-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled:hover {
|
||||||
|
background-color: var(--accent-font-dark);
|
||||||
|
transform: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.video-item.table {
|
.video-item.table {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue