From 0751bd371ac4d2dc577f71d790a8786f5c22a71e Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Mon, 27 Oct 2025 14:01:07 +0100 Subject: [PATCH 1/5] OpenAPI specification, fixing enum for notification type, and notification_muted (#3557) Re #3556 --- docs/api-spec.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index c47172511..e3bb8bd5f 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -143,7 +143,7 @@ components: paused: type: boolean description: Whether the web page change monitor (watch) is paused - muted: + notification_muted: type: boolean description: Whether notifications are muted method: @@ -207,7 +207,7 @@ components: maxLength: 5000 notification_format: type: string - enum: [Text, HTML, Markdown] + enum: ['Plain Text', 'HTML', 'HTML Color', 'Markdown to HTML', 'System default'] description: Format for notifications track_ldjson_price_data: type: boolean @@ -406,7 +406,7 @@ paths: page_title: "The HTML from the page" tags: ["550e8400-e29b-41d4-a716-446655440000"] paused: false - muted: false + notification_muted: false method: "GET" fetch_backend: "html_requests" last_checked: 1640995200 @@ -419,7 +419,7 @@ paths: page_title: "The HTML <title> from the page" tags: ["330e8400-e29b-41d4-a716-446655440001"] paused: false - muted: true + notification_muted: true method: "GET" fetch_backend: "html_webdriver" last_checked: 1640998800 @@ -1224,7 +1224,7 @@ paths: title: "Example Website Monitor" tags: ["550e8400-e29b-41d4-a716-446655440000"] paused: false - muted: false + notification_muted: false /import: post: From 2db5e906e9e01f11a91376f436a92081d66ef0fe Mon Sep 17 00:00:00 2001 From: dgtlmoon <dgtlmoon@gmail.com> Date: Mon, 27 Oct 2025 16:46:56 +0100 Subject: [PATCH 2/5] Update 21 for #3496 - Fixing update of timezone setting --- changedetectionio/store.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changedetectionio/store.py b/changedetectionio/store.py index 893cdac34..033de4ddc 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -987,8 +987,9 @@ class ChangeDetectionStore: self.data['settings']['application']['ui']['use_page_title_in_list'] = self.data['settings']['application'].get('extract_title_as_title') def update_21(self): - self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') - del self.data['settings']['application']['timezone'] + if self.data['settings']['application'].get('timezone'): + self.data['settings']['application']['scheduler_timezone_default'] = self.data['settings']['application'].get('timezone') + del self.data['settings']['application']['timezone'] def add_notification_url(self, notification_url): From c9290d73e0d3c777fc9394efc4da10aa3fb55171 Mon Sep 17 00:00:00 2001 From: dgtlmoon <dgtlmoon@gmail.com> Date: Mon, 27 Oct 2025 17:08:05 +0100 Subject: [PATCH 3/5] HTML - Shorten whitespace around timezone names --- changedetectionio/blueprint/settings/templates/settings.html | 4 +--- changedetectionio/templates/_helpers.html | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html index d933f9383..9913e0958 100644 --- a/changedetectionio/blueprint/settings/templates/settings.html +++ b/changedetectionio/blueprint/settings/templates/settings.html @@ -240,9 +240,7 @@ nav <p> {{ render_field(form.application.form.scheduler_timezone_default) }} <datalist id="timezones" style="display: none;"> - {% for tz_name in available_timezones %} - <option value="{{ tz_name }}">{{ tz_name }}</option> - {% endfor %} + {%- for timezone in available_timezones -%}<option value="{{ timezone }}">{{ timezone }}</option>{%- endfor -%} </datalist> </p> </div> diff --git a/changedetectionio/templates/_helpers.html b/changedetectionio/templates/_helpers.html index c5c061557..d0030d680 100644 --- a/changedetectionio/templates/_helpers.html +++ b/changedetectionio/templates/_helpers.html @@ -266,9 +266,7 @@ <li id="timezone-info"> {{ render_field(form.time_schedule_limit.timezone, placeholder=timezone_default_config) }} <span id="local-time-in-tz"></span> <datalist id="timezones" style="display: none;"> - {% for timezone in available_timezones %} - <option value="{{ timezone }}">{{ timezone }}</option> - {% endfor %} + {%- for timezone in available_timezones -%}<option value="{{ timezone }}">{{ timezone }}</option>{%- endfor -%} </datalist> </li> </ul> From a8cadc3d1652bd6835ac044c620e510d7953c043 Mon Sep 17 00:00:00 2001 From: dgtlmoon <dgtlmoon@gmail.com> Date: Mon, 27 Oct 2025 18:56:01 +0100 Subject: [PATCH 4/5] Fixing wrong notification type in <select> that lead to wrong type of notifications (plaintext vs html) being sent #3558 (#3559) --- changedetectionio/blueprint/ui/__init__.py | 4 +- changedetectionio/content_fetchers/base.py | 1 - changedetectionio/forms.py | 3 +- changedetectionio/model/__init__.py | 4 +- changedetectionio/notification/__init__.py | 15 ++++--- changedetectionio/notification/handler.py | 19 +++++---- changedetectionio/notification_service.py | 39 ++++++++++++------- changedetectionio/store.py | 26 ++++++++++++- .../tests/smtp/test_notification_smtp.py | 20 +++++----- .../tests/test_add_replace_remove_filter.py | 2 +- .../tests/test_filter_exist_changes.py | 2 +- .../tests/test_filter_failure_notification.py | 8 ++-- changedetectionio/tests/test_group.py | 2 +- changedetectionio/tests/test_notification.py | 18 ++++++--- .../tests/test_notification_errors.py | 2 +- changedetectionio/widgets/test_custom_text.py | 5 ++- docs/api-spec.yaml | 4 +- 17 files changed, 109 insertions(+), 65 deletions(-) diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index ce4bb716a..7755d2ca8 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -76,14 +76,14 @@ def _handle_operations(op, uuids, datastore, worker_handler, update_q, queuedWat elif (op == 'notification-default'): from changedetectionio.notification import ( - default_notification_format_for_watch + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH ) for uuid in uuids: if datastore.data['watching'].get(uuid): datastore.data['watching'][uuid]['notification_title'] = None datastore.data['watching'][uuid]['notification_body'] = None datastore.data['watching'][uuid]['notification_urls'] = [] - datastore.data['watching'][uuid]['notification_format'] = default_notification_format_for_watch + datastore.data['watching'][uuid]['notification_format'] = USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if emit_flash: flash(f"{len(uuids)} watches set to use default notification settings") diff --git a/changedetectionio/content_fetchers/base.py b/changedetectionio/content_fetchers/base.py index a6fe2005b..6d7d3d5d8 100644 --- a/changedetectionio/content_fetchers/base.py +++ b/changedetectionio/content_fetchers/base.py @@ -75,7 +75,6 @@ class Fetcher(): self.screenshot = None self.xpath_data = None # Keep headers and status_code as they're small - logger.trace("Fetcher content cleared from memory") @abstractmethod def get_error(self): diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py index 836f90658..148731956 100644 --- a/changedetectionio/forms.py +++ b/changedetectionio/forms.py @@ -741,7 +741,6 @@ class quickWatchForm(Form): edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) - # Common to a single watch and the global settings class commonSettingsForm(Form): from . import processors @@ -754,7 +753,7 @@ class commonSettingsForm(Form): fetch_backend = RadioField(u'Fetch Method', choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()]) notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()]) - notification_format = SelectField('Notification format', choices=valid_notification_formats.keys()) + notification_format = SelectField('Notification format', choices=list(valid_notification_formats.items())) notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()]) notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers(), ValidateJinja2Template()]) processor = RadioField( label=u"Processor - What do you want to achieve?", choices=processors.available_processors(), default="text_json_diff") diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index 222dafc70..cfc8aee64 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -2,7 +2,7 @@ import os import uuid from changedetectionio import strtobool -default_notification_format_for_watch = 'System default' +USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH = 'System default' CONDITIONS_MATCH_LOGIC_DEFAULT = 'ALL' class watch_base(dict): @@ -44,7 +44,7 @@ class watch_base(dict): 'method': 'GET', 'notification_alert_count': 0, 'notification_body': None, - 'notification_format': default_notification_format_for_watch, + 'notification_format': USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, 'notification_muted': False, 'notification_screenshot': False, # Include the latest screenshot if available and supported by the apprise URL 'notification_title': None, diff --git a/changedetectionio/notification/__init__.py b/changedetectionio/notification/__init__.py index 06ed830a8..e82687871 100644 --- a/changedetectionio/notification/__init__.py +++ b/changedetectionio/notification/__init__.py @@ -1,17 +1,16 @@ -from changedetectionio.model import default_notification_format_for_watch +from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH -default_notification_format = 'HTML Color' +default_notification_format = 'htmlcolor' default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' # The values (markdown etc) are from apprise NotifyFormat, # But to avoid importing the whole heavy module just use the same strings here. valid_notification_formats = { - 'Plain Text': 'text', - 'HTML': 'html', - 'HTML Color': 'htmlcolor', - 'Markdown to HTML': 'markdown', + 'text': 'Plain Text', + 'html': 'HTML', + 'htmlcolor': 'HTML Color', + 'markdown': 'Markdown to HTML', # Used only for editing a watch (not for global) - default_notification_format_for_watch: default_notification_format_for_watch + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH } - diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index cd2782cbb..eba35b710 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -63,13 +63,13 @@ def notification_format_align_with_apprise(n_format : str): :return: """ - if n_format.lower().startswith('html'): + if n_format.startswith('html'): # Apprise only knows 'html' not 'htmlcolor' etc, which shouldnt matter here n_format = NotifyFormat.HTML.value - elif n_format.lower().startswith('markdown'): + elif n_format.startswith('markdown'): # probably the same but just to be safe n_format = NotifyFormat.MARKDOWN.value - elif n_format.lower().startswith('text'): + elif n_format.startswith('text'): # probably the same but just to be safe n_format = NotifyFormat.TEXT.value else: @@ -241,7 +241,7 @@ def apply_service_tweaks(url, n_body, n_title, requested_output_format): def process_notification(n_object: NotificationContextData, datastore): from changedetectionio.jinja2_custom import render as jinja_render - from . import default_notification_format_for_watch, default_notification_format, valid_notification_formats + from . import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_format, valid_notification_formats # be sure its registered from .apprise_plugin.custom_handlers import apprise_http_custom_handler # Register custom Discord plugin @@ -257,18 +257,17 @@ def process_notification(n_object: NotificationContextData, datastore): # Insert variables into the notification content notification_parameters = create_notification_parameters(n_object, datastore) - requested_output_format = valid_notification_formats.get( - n_object.get('notification_format', default_notification_format), - valid_notification_formats[default_notification_format], - ) + requested_output_format = n_object.get('notification_format', default_notification_format) + logger.debug(f"Requested notification output format: '{requested_output_format}'") # If we arrived with 'System default' then look it up - if requested_output_format == default_notification_format_for_watch and datastore.data['settings']['application'].get('notification_format') != default_notification_format_for_watch: + if requested_output_format == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # Initially text or whatever - requested_output_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]).lower() + requested_output_format = datastore.data['settings']['application'].get('notification_format', default_notification_format) requested_output_format_original = requested_output_format + # Now clean it up so it fits perfectly with apprise requested_output_format = notification_format_align_with_apprise(n_format=requested_output_format) logger.trace(f"Complete notification body including Jinja and placeholders calculated in {time.time() - now:.2f}s") diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index 2a987bb5e..f144c5130 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -9,7 +9,8 @@ for both sync and async workers from loguru import logger import time -from changedetectionio.notification import default_notification_format +from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH +from changedetectionio.notification import default_notification_format, valid_notification_formats # This gets modified on notification time (handler.py) depending on the required notification output CUSTOM_LINEBREAK_PLACEHOLDER='@BR@' @@ -48,15 +49,28 @@ class NotificationContextData(dict): if kwargs: self.update(kwargs) + n_format = self.get('notification_format') + if n_format and not valid_notification_formats.get(n_format): + raise ValueError(f'Invalid notification format: "{n_format}"') + def set_random_for_validation(self): import random, string - """Randomly fills all dict keys with random strings (for validation/testing).""" + """Randomly fills all dict keys with random strings (for validation/testing). + So we can test the output in the notification body + """ for key in self.keys(): if key in ['uuid', 'time', 'watch_uuid']: continue rand_str = 'RANDOM-PLACEHOLDER-'+''.join(random.choices(string.ascii_letters + string.digits, k=12)) self[key] = rand_str + def __setitem__(self, key, value): + if key == 'notification_format' and isinstance(value, str) and not value.startswith('RANDOM-PLACEHOLDER-'): + if not valid_notification_formats.get(value): + raise ValueError(f'Invalid notification format: "{value}"') + + super().__setitem__(key, value) + class NotificationService: """ Standalone notification service that handles all notification functionality @@ -72,7 +86,7 @@ class NotificationService: Queue a notification for a watch with full diff rendering and template variables """ from changedetectionio import diff - from changedetectionio.notification import default_notification_format_for_watch + from changedetectionio.notification import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if not isinstance(n_object, NotificationContextData): raise TypeError(f"Expected NotificationContextData, got {type(n_object)}") @@ -94,7 +108,7 @@ class NotificationService: snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." # If we ended up here with "System default" - if n_object.get('notification_format') == default_notification_format_for_watch: + if n_object.get('notification_format') == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format') @@ -141,7 +155,7 @@ class NotificationService: Individual watch settings > Tag settings > Global settings """ from changedetectionio.notification import ( - default_notification_format_for_watch, + USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH, default_notification_body, default_notification_title ) @@ -149,7 +163,7 @@ class NotificationService: # Would be better if this was some kind of Object where Watch can reference the parent datastore etc v = watch.get(var_name) if v and not watch.get('notification_muted'): - if var_name == 'notification_format' and v == default_notification_format_for_watch: + if var_name == 'notification_format' and v == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: return self.datastore.data['settings']['application'].get('notification_format') return v @@ -166,7 +180,7 @@ class NotificationService: # Otherwise could be defaults if var_name == 'notification_format': - return default_notification_format_for_watch + return USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH if var_name == 'notification_body': return default_notification_body if var_name == 'notification_title': @@ -221,7 +235,6 @@ class NotificationService: if not watch: return - n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format) filter_list = ", ".join(watch['include_filters']) # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed body = f"""Hello, @@ -238,9 +251,9 @@ Thanks - Your omniscient changedetection.io installation. n_object = NotificationContextData({ 'notification_title': 'Changedetection.io - Alert - CSS/xPath filter was not present in the page', 'notification_body': body, - 'notification_format': n_format, - 'markup_text_links_to_html_links': n_format.lower().startswith('html') + 'notification_format': self._check_cascading_vars('notification_format', watch), }) + n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] @@ -268,7 +281,7 @@ Thanks - Your omniscient changedetection.io installation. if not watch: return threshold = self.datastore.data['settings']['application'].get('filter_failure_notification_threshold_attempts') - n_format = self.datastore.data['settings']['application'].get('notification_format', default_notification_format).lower() + step = step_n + 1 # @todo - This could be a markdown template on the disk, apprise will convert the markdown to HTML+Plaintext parts in the email, and then 'markup_text_links_to_html_links' is not needed @@ -287,9 +300,9 @@ Thanks - Your omniscient changedetection.io installation. n_object = NotificationContextData({ 'notification_title': f"Changedetection.io - Alert - Browser step at position {step} could not be run", 'notification_body': body, - 'notification_format': n_format, - 'markup_text_links_to_html_links': n_format.lower().startswith('html') + 'notification_format': self._check_cascading_vars('notification_format', watch), }) + n_object['markup_text_links_to_html_links'] = n_object.get('notification_format').startswith('html') if len(watch['notification_urls']): n_object['notification_urls'] = watch['notification_urls'] diff --git a/changedetectionio/store.py b/changedetectionio/store.py index 033de4ddc..38009a736 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -5,7 +5,7 @@ from flask import ( ) from .html_tools import TRANSLATE_WHITESPACE_TABLE -from . model import App, Watch +from .model import App, Watch, USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH from copy import deepcopy, copy from os import path, unlink from threading import Lock @@ -992,6 +992,30 @@ class ChangeDetectionStore: del self.data['settings']['application']['timezone'] + # Some notification formats got the wrong name type + def update_22(self): + from .notification import valid_notification_formats + + sys_n_format = self.data['settings']['application'].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == sys_n_format), None) + if key_exists_as_value: # key of "Plain text" + logger.success(f"['settings']['application']['notification_format'] '{sys_n_format}' -> '{key_exists_as_value}'") + self.data['settings']['application']['notification_format'] = key_exists_as_value + + for uuid, watch in self.data['watching'].items(): + n_format = self.data['watching'][uuid].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) + if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # key of "Plain text" + logger.success(f"['watching'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") + self.data['watching'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever + + for uuid, tag in self.data['settings']['application']['tags'].items(): + n_format = self.data['settings']['application']['tags'][uuid].get('notification_format') + key_exists_as_value = next((k for k, v in valid_notification_formats.items() if v == n_format), None) + if key_exists_as_value and key_exists_as_value != USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: # key of "Plain text" + logger.success(f"['settings']['application']['tags'][{uuid}]['notification_format'] '{n_format}' -> '{key_exists_as_value}'") + self.data['settings']['application']['tags'][uuid]['notification_format'] = key_exists_as_value # should be 'text' or whatever + def add_notification_url(self, notification_url): logger.debug(f">>> Adding new notification_url - '{notification_url}'") diff --git a/changedetectionio/tests/smtp/test_notification_smtp.py b/changedetectionio/tests/smtp/test_notification_smtp.py index a486fd858..a0ed0c15c 100644 --- a/changedetectionio/tests/smtp/test_notification_smtp.py +++ b/changedetectionio/tests/smtp/test_notification_smtp.py @@ -53,7 +53,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": "some text\nfallback-body<br> " + default_notification_body, - "application-notification_format": 'HTML', + "application-notification_format": 'html', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -122,7 +122,7 @@ def test_check_notification_plaintext_format(client, live_server, measure_memory data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": "some text\n" + default_notification_body, - "application-notification_format": 'Plain Text', + "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -174,7 +174,7 @@ def test_check_notification_html_color_format(client, live_server, measure_memor data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"some text\n{default_notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", - "application-notification_format": 'HTML Color', + "application-notification_format": 'htmlcolor', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -245,7 +245,7 @@ def test_check_notification_markdown_format(client, live_server, measure_memory_ data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": "*header*\n\nsome text\n" + default_notification_body, - "application-notification_format": 'Markdown to HTML', + "application-notification_format": 'markdown', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -329,7 +329,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": notification_body, - "application-notification_format": 'Plain Text', + "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -379,7 +379,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv url_for("ui.ui_edit.edit_page", uuid="first"), data={ "url": test_url, - "notification_format": 'HTML', + "notification_format": 'html', 'fetch_backend': "html_requests", "time_between_check_use_default": "y"}, follow_redirects=True @@ -438,7 +438,7 @@ def test_check_plaintext_document_plaintext_notification_smtp(client, live_serve data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", - "application-notification_format": 'Plain Text', + "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -490,7 +490,7 @@ def test_check_plaintext_document_html_notifications(client, live_server, measur data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", - "application-notification_format": 'HTML', + "application-notification_format": 'html', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -568,7 +568,7 @@ def test_check_plaintext_document_html_color_notifications(client, live_server, data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", - "application-notification_format": 'HTML Color', + "application-notification_format": 'htmlcolor', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True @@ -640,7 +640,7 @@ def test_check_html_document_plaintext_notification(client, live_server, measure data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": f"{notification_body}\nMore output test\n{ALL_MARKUP_TOKENS}", - "application-notification_format": 'Plain Text', + "application-notification_format": 'text', "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True diff --git a/changedetectionio/tests/test_add_replace_remove_filter.py b/changedetectionio/tests/test_add_replace_remove_filter.py index 72987e945..ef38b9ad5 100644 --- a/changedetectionio/tests/test_add_replace_remove_filter.py +++ b/changedetectionio/tests/test_add_replace_remove_filter.py @@ -124,7 +124,7 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa "application-notification_body": 'triggered text was -{{triggered_text}}- ### 网站监测 内容更新了 ####', # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation "application-notification_urls": test_notification_url, - "application-notification_format": 'Plain Text', + "application-notification_format": 'text', "application-minutes_between_check": 180, "application-fetch_backend": "html_requests" }, diff --git a/changedetectionio/tests/test_filter_exist_changes.py b/changedetectionio/tests/test_filter_exist_changes.py index 261a1fdd8..157dc786f 100644 --- a/changedetectionio/tests/test_filter_exist_changes.py +++ b/changedetectionio/tests/test_filter_exist_changes.py @@ -86,7 +86,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se "Diff Full: {{diff_full}}\n" "Diff as Patch: {{diff_patch}}\n" ":-)", - "notification_format": 'Plain Text'} + "notification_format": 'text'} notification_form_data.update({ "url": test_url, diff --git a/changedetectionio/tests/test_filter_failure_notification.py b/changedetectionio/tests/test_filter_failure_notification.py index 968f928f5..875e0b210 100644 --- a/changedetectionio/tests/test_filter_failure_notification.py +++ b/changedetectionio/tests/test_filter_failure_notification.py @@ -63,7 +63,7 @@ def run_filter_test(client, live_server, content_filter, app_notification_format "Diff Full: {{diff_full}}\n" "Diff as Patch: {{diff_patch}}\n" ":-)", - "notification_format": 'Plain Text', + "notification_format": 'text', "fetch_backend": "html_requests", "filter_failure_notification_send": 'y', "time_between_check_use_default": "y", @@ -175,13 +175,13 @@ def run_filter_test(client, live_server, content_filter, app_notification_format def test_check_include_filters_failure_notification(client, live_server, measure_memory_usage): # # live_server_setup(live_server) # Setup on conftest per function - run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('HTML Color')) + run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('htmlcolor')) # Check markup send conversion didnt affect plaintext preference - run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('Plain Text')) + run_filter_test(client=client, live_server=live_server, content_filter='#nope-doesnt-exist', app_notification_format=valid_notification_formats.get('text')) def test_check_xpath_filter_failure_notification(client, live_server, measure_memory_usage): # # live_server_setup(live_server) # Setup on conftest per function - run_filter_test(client=client, live_server=live_server, content_filter='//*[@id="nope-doesnt-exist"]', app_notification_format=valid_notification_formats.get('HTML Color')) + run_filter_test(client=client, live_server=live_server, content_filter='//*[@id="nope-doesnt-exist"]', app_notification_format=valid_notification_formats.get('htmlcolor')) # Test that notification is never sent diff --git a/changedetectionio/tests/test_group.py b/changedetectionio/tests/test_group.py index 3789ef8f2..186a641c2 100644 --- a/changedetectionio/tests/test_group.py +++ b/changedetectionio/tests/test_group.py @@ -195,7 +195,7 @@ def test_group_tag_notification(client, live_server, measure_memory_usage): "Diff as Patch: {{diff_patch}}\n" ":-)", "notification_screenshot": True, - "notification_format": 'Plain Text', + "notification_format": 'text', "title": "test-tag"} res = client.post( diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index 4de145e0c..b9e418a26 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -13,10 +13,10 @@ import base64 from changedetectionio.notification import ( default_notification_body, default_notification_format, - default_notification_title, - valid_notification_formats, + default_notification_title, valid_notification_formats ) from ..diff import HTML_CHANGED_STYLE +from ..model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH # Hard to just add more live server URLs when one test is already running (I think) @@ -47,6 +47,14 @@ def test_check_notification(client, live_server, measure_memory_usage): assert b"Settings updated." in res.data + res = client.get(url_for("settings.settings_page")) + for k,v in valid_notification_formats.items(): + if k == USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH: + continue + assert f'value="{k}"'.encode() in res.data # Should be by key NOT value + assert f'value="{v}"'.encode() not in res.data # Should be by key NOT value + + # When test mode is in BASE_URL env mode, we should see this already configured env_base_url = os.getenv('BASE_URL', '').strip() if len(env_base_url): @@ -101,7 +109,7 @@ def test_check_notification(client, live_server, measure_memory_usage): "Diff as Patch: {{diff_patch}}\n" ":-)", "notification_screenshot": True, - "notification_format": 'Plain Text'} + "notification_format": 'text'} notification_form_data.update({ "url": test_url, @@ -267,7 +275,7 @@ def test_notification_validation(client, live_server, measure_memory_usage): # data={"notification_urls": 'json://localhost/foobar', # "notification_title": "", # "notification_body": "", -# "notification_format": 'Plain Text', +# "notification_format": 'text', # "url": test_url, # "tag": "my tag", # "title": "my title", @@ -521,7 +529,7 @@ def _test_color_notifications(client, notification_body_token): "application-fetch_backend": "html_requests", "application-minutes_between_check": 180, "application-notification_body": notification_body_token, - "application-notification_format": "HTML Color", + "application-notification_format": "htmlcolor", "application-notification_urls": test_notification_url, "application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", }, diff --git a/changedetectionio/tests/test_notification_errors.py b/changedetectionio/tests/test_notification_errors.py index f33d42b58..389670019 100644 --- a/changedetectionio/tests/test_notification_errors.py +++ b/changedetectionio/tests/test_notification_errors.py @@ -30,7 +30,7 @@ def test_check_notification_error_handling(client, live_server, measure_memory_u data={"notification_urls": f"{broken_notification_url}\r\n{working_notification_url}", "notification_title": "xxx", "notification_body": "xxxxx", - "notification_format": 'Plain Text', + "notification_format": 'text', "url": test_url, "tags": "", "title": "", diff --git a/changedetectionio/widgets/test_custom_text.py b/changedetectionio/widgets/test_custom_text.py index 8f04a8caf..5dad06ae8 100644 --- a/changedetectionio/widgets/test_custom_text.py +++ b/changedetectionio/widgets/test_custom_text.py @@ -2,6 +2,9 @@ import sys import os + +from changedetectionio.model import USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..')) from changedetectionio.widgets import TernaryNoneBooleanField @@ -93,7 +96,7 @@ def test_custom_text(): print(f"Does NOT contain 'System default': {'System default' not in boolean_html}") print(f"Does NOT contain 'Default': {'Default' not in boolean_html}") assert 'Enabled' in boolean_html and 'Disabled' in boolean_html - assert 'System default' not in boolean_html and 'Default' not in boolean_html + assert USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH not in boolean_html and 'Default' not in boolean_html # Test FontAwesome field print("\n--- FontAwesome Icons Field ---") diff --git a/docs/api-spec.yaml b/docs/api-spec.yaml index e3bb8bd5f..e602bd7e1 100644 --- a/docs/api-spec.yaml +++ b/docs/api-spec.yaml @@ -28,7 +28,7 @@ info: For example: `x-api-key: YOUR_API_KEY` - version: 0.1.1 + version: 0.1.2 contact: name: ChangeDetection.io url: https://github.com/dgtlmoon/changedetection.io @@ -207,7 +207,7 @@ components: maxLength: 5000 notification_format: type: string - enum: ['Plain Text', 'HTML', 'HTML Color', 'Markdown to HTML', 'System default'] + enum: ['text', 'html', 'htmlcolor', 'markdown', 'System default'] description: Format for notifications track_ldjson_price_data: type: boolean From 8f580ac96bc85307d66948fd0495a11f5f15836a Mon Sep 17 00:00:00 2001 From: dgtlmoon <dgtlmoon@gmail.com> Date: Mon, 27 Oct 2025 18:56:51 +0100 Subject: [PATCH 5/5] 0.50.33 --- changedetectionio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index db0567ac4..36753e195 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -2,7 +2,7 @@ # Read more https://github.com/dgtlmoon/changedetection.io/wiki -__version__ = '0.50.32' +__version__ = '0.50.33' from changedetectionio.strtobool import strtobool from json.decoder import JSONDecodeError