diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py index ea623c6d..d00205d5 100644 --- a/changedetectionio/blueprint/settings/__init__.py +++ b/changedetectionio/blueprint/settings/__init__.py @@ -20,6 +20,14 @@ def construct_blueprint(datastore: ChangeDetectionStore): from changedetectionio.blueprint.settings.llm import construct_llm_blueprint settings_blueprint.register_blueprint(construct_llm_blueprint(datastore), url_prefix='/llm') + # Notification settings live in their own child blueprint so future backends + # (simple_email, webhooks, etc.) slot in as /settings/notifications/ + # without further URL-shaping churn. + from changedetectionio.blueprint.settings.notifications import construct_notifications_blueprint + settings_blueprint.register_blueprint( + construct_notifications_blueprint(datastore), url_prefix='/notifications', + ) + @settings_blueprint.route("", methods=['GET', "POST"]) @login_optionally_required def settings_page(): @@ -277,46 +285,6 @@ def construct_blueprint(datastore: ChangeDetectionStore): flash(gettext("API Key was regenerated.")) return redirect(url_for('settings.settings_page')+'#api') - @settings_blueprint.route("/notifications", methods=['GET', 'POST']) - @login_optionally_required - def notifications_page(): - from changedetectionio import forms - - app_settings = datastore.data['settings']['application'] - # Seed the form with the currently stored values so GET (and a failed - # POST that re-renders) shows what's persisted, not WTForms defaults. - default = { - 'notification_urls': app_settings.get('notification_urls') or [], - 'notification_title': app_settings.get('notification_title') or '', - 'notification_body': app_settings.get('notification_body') or '', - 'notification_format': app_settings.get('notification_format') or '', - 'base_url': app_settings.get('base_url') or '', - } - - form = forms.globalSettingsNotificationForm( - formdata=request.form if request.method == 'POST' else None, - data=default, - extra_notification_tokens=datastore.get_unique_notification_tokens_available(), - ) - - if request.method == 'POST' and form.validate(): - for field in ('notification_urls', 'notification_title', 'notification_body', - 'notification_format', 'base_url'): - app_settings[field] = form.data.get(field) - datastore.commit() - flash(gettext("Settings updated.")) - return redirect(url_for('settings.notifications_page')) - elif request.method == 'POST': - flash(gettext("An error occurred, please see below."), "error") - - return render_template( - "notifications.html", - form=form, - emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), - extra_notification_token_placeholder_info=datastore.get_unique_notification_token_placeholders_available(), - settings_application=app_settings, - ) - @settings_blueprint.route("/notification-logs", methods=['GET']) @login_optionally_required def notification_logs(): diff --git a/changedetectionio/blueprint/settings/notifications/__init__.py b/changedetectionio/blueprint/settings/notifications/__init__.py new file mode 100644 index 00000000..7bebc327 --- /dev/null +++ b/changedetectionio/blueprint/settings/notifications/__init__.py @@ -0,0 +1,76 @@ +""" +Notification settings — child blueprint of /settings. + +Registered by the parent settings blueprint at url_prefix='/notifications', so the +URLs land at /settings/notifications/. Today the only backend is apprise; +future backends (simple_email, raw webhooks, etc.) slot in as sibling routes here +without re-shaping the URL space again. + +The eventual data-model refactor is to give each notification config its own +uuid (`notification_uuid`) referenced from global/watch/group settings, instead +of inlining apprise URL strings on every record — that's not done here; this +file just gives that future work a stable home. +""" +import os + +from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask_babel import gettext + +from changedetectionio.store import ChangeDetectionStore +from changedetectionio.auth_decorator import login_optionally_required + + +def construct_notifications_blueprint(datastore: ChangeDetectionStore): + notifications_blueprint = Blueprint( + 'notifications', __name__, + template_folder="templates", + ) + + @notifications_blueprint.route("/", methods=['GET']) + @login_optionally_required + def index(): + # /settings/notifications/ → apprise (only backend for now). + # When a second backend lands this becomes a chooser. + return redirect(url_for('settings.notifications.apprise')) + + @notifications_blueprint.route("/apprise", methods=['GET', 'POST']) + @login_optionally_required + def apprise(): + from changedetectionio import forms + + app_settings = datastore.data['settings']['application'] + # Seed the form with the currently stored values so GET (and a failed + # POST that re-renders) shows what's persisted, not WTForms defaults. + default = { + 'notification_urls': app_settings.get('notification_urls') or [], + 'notification_title': app_settings.get('notification_title') or '', + 'notification_body': app_settings.get('notification_body') or '', + 'notification_format': app_settings.get('notification_format') or '', + 'base_url': app_settings.get('base_url') or '', + } + + form = forms.globalSettingsAppriseNotificationForm( + formdata=request.form if request.method == 'POST' else None, + data=default, + extra_notification_tokens=datastore.get_unique_notification_tokens_available(), + ) + + if request.method == 'POST' and form.validate(): + for field in ('notification_urls', 'notification_title', 'notification_body', + 'notification_format', 'base_url'): + app_settings[field] = form.data.get(field) + datastore.commit() + flash(gettext("Settings updated.")) + return redirect(url_for('settings.notifications.apprise')) + elif request.method == 'POST': + flash(gettext("An error occurred, please see below."), "error") + + return render_template( + "apprise.html", + form=form, + emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False), + extra_notification_token_placeholder_info=datastore.get_unique_notification_token_placeholders_available(), + settings_application=app_settings, + ) + + return notifications_blueprint diff --git a/changedetectionio/blueprint/settings/templates/notifications.html b/changedetectionio/blueprint/settings/notifications/templates/apprise.html similarity index 97% rename from changedetectionio/blueprint/settings/templates/notifications.html rename to changedetectionio/blueprint/settings/notifications/templates/apprise.html index 5d38e0e9..ca75dd37 100644 --- a/changedetectionio/blueprint/settings/templates/notifications.html +++ b/changedetectionio/blueprint/settings/notifications/templates/apprise.html @@ -18,7 +18,7 @@
-
+
diff --git a/changedetectionio/blueprint/tags/templates/edit-tag.html b/changedetectionio/blueprint/tags/templates/edit-tag.html index fcefad8f..a6811a53 100644 --- a/changedetectionio/blueprint/tags/templates/edit-tag.html +++ b/changedetectionio/blueprint/tags/templates/edit-tag.html @@ -131,7 +131,7 @@ {% if has_default_notification_urls %}
{{ _('Look out!') }} - {{ _('There are system-wide notification URLs enabled, this form will override notification settings for this watch only ‐ an empty Notification URL list here will still send notifications.', url=url_for('settings.notifications_page'))|safe }} + {{ _('There are system-wide notification URLs enabled, this form will override notification settings for this watch only ‐ an empty Notification URL list here will still send notifications.', url=url_for('settings.notifications.apprise'))|safe }}
{% endif %} {{ _('Use system defaults') }} diff --git a/changedetectionio/blueprint/ui/templates/edit.html b/changedetectionio/blueprint/ui/templates/edit.html index d6ed4f68..aead58ad 100644 --- a/changedetectionio/blueprint/ui/templates/edit.html +++ b/changedetectionio/blueprint/ui/templates/edit.html @@ -299,7 +299,7 @@ Math: {{ 1 + 1 }}") }} {% if has_default_notification_urls %}
{{ _('Look out!') }} - {{ _('There are system-wide notification URLs enabled, this form will override notification settings for this watch only ‐ an empty Notification URL list here will still send notifications.', url=url_for('settings.notifications_page'))|safe }} + {{ _('There are system-wide notification URLs enabled, this form will override notification settings for this watch only ‐ an empty Notification URL list here will still send notifications.', url=url_for('settings.notifications.apprise'))|safe }}
{% endif %} {{ _('Use system defaults') }} diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 267fddb0..7f1fbc47 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -809,15 +809,19 @@ class commonSettingsForm(Form): # raise ValidationError('HTML Color format is not supported by Telegram and Discord. Please choose another Notification Format (Plain Text, HTML, or Markdown to HTML).') -# Standalone form for the /settings/notifications page. Holds only the global -# notification fields (the macro `notification_part_render_common_settings_form` -# in _common_fields.html expects these exact field names) plus base_url, which -# powers the {{base_url}} token in notification templates. +# Standalone form for the /settings/notifications/apprise page. Holds only the +# global apprise-notification fields (the macro +# `notification_part_render_common_settings_form` in _common_fields.html expects +# these exact field names) plus base_url, which powers the {{base_url}} token +# in notification templates. # # This is intentionally not a sub-form of globalSettingsForm — the notifications # page POSTs to its own route so the broader settings form's validators (worker # count, RSS limits, etc.) don't run when the user only wants to tweak alerts. -class globalSettingsNotificationForm(Form): +# +# Named for the backend (apprise) on purpose: future backends (simple_email, +# webhook, etc.) will land as their own forms next to this one. +class globalSettingsAppriseNotificationForm(Form): def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs): super().__init__(formdata, obj, prefix, data, meta, **kwargs) extra = kwargs.get('extra_notification_tokens', {}) diff --git a/changedetectionio/templates/sidebar-nav.html b/changedetectionio/templates/sidebar-nav.html index 56af2642..6863865d 100644 --- a/changedetectionio/templates/sidebar-nav.html +++ b/changedetectionio/templates/sidebar-nav.html @@ -82,7 +82,7 @@
  • -