mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-02-01 11:56:03 +00:00
Compare commits
1 Commits
python-314
...
3740-html-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ce040397 |
@@ -86,6 +86,10 @@
|
|||||||
<div class="tab-pane-inner" id="notifications">
|
<div class="tab-pane-inner" id="notifications">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{{ render_common_settings_form(form.application.form, emailprefix, settings_application, extra_notification_token_placeholder_info) }}
|
{{ render_common_settings_form(form.application.form, emailprefix, settings_application, extra_notification_token_placeholder_info) }}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="pure-group">
|
||||||
|
{{ render_checkbox_field(form.application.form.notification_html_word_diff_enabled) }}
|
||||||
|
<span class="pure-form-message-inline">HTML notifications - Use "word by word" difference where possible.</span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="pure-control-group" id="notification-base-url">
|
<div class="pure-control-group" id="notification-base-url">
|
||||||
{{ render_field(form.application.form.base_url, class="m-d") }}
|
{{ render_field(form.application.form.base_url, class="m-d") }}
|
||||||
|
|||||||
@@ -991,6 +991,7 @@ class globalSettingsApplicationForm(commonSettingsForm):
|
|||||||
render_kw={"placeholder": os.getenv('BASE_URL', 'Not set')}
|
render_kw={"placeholder": os.getenv('BASE_URL', 'Not set')}
|
||||||
)
|
)
|
||||||
empty_pages_are_a_change = BooleanField(_l('Treat empty pages as a change?'), default=False)
|
empty_pages_are_a_change = BooleanField(_l('Treat empty pages as a change?'), default=False)
|
||||||
|
notification_html_word_diff_enabled = BooleanField(_l('Notification HTML as word-by-word difference'), default=True, validators=[validators.Optional()])
|
||||||
fetch_backend = RadioField(_l('Fetch Method'), default="html_requests", choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
|
fetch_backend = RadioField(_l('Fetch Method'), default="html_requests", choices=content_fetchers.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
|
||||||
global_ignore_text = StringListField(_l('Ignore Text'), [ValidateListRegex()])
|
global_ignore_text = StringListField(_l('Ignore Text'), [ValidateListRegex()])
|
||||||
global_subtractive_selectors = StringListField(_l('Remove elements'), [ValidateCSSJSONXPATHInput(allow_json=False)])
|
global_subtractive_selectors = StringListField(_l('Remove elements'), [ValidateCSSJSONXPATHInput(allow_json=False)])
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class model(dict):
|
|||||||
'ssim_threshold': '0.96', # Default SSIM threshold for screenshot comparison
|
'ssim_threshold': '0.96', # Default SSIM threshold for screenshot comparison
|
||||||
'notification_body': default_notification_body,
|
'notification_body': default_notification_body,
|
||||||
'notification_format': default_notification_format,
|
'notification_format': default_notification_format,
|
||||||
|
'notification_html_word_diff': True,
|
||||||
'notification_title': default_notification_title,
|
'notification_title': default_notification_title,
|
||||||
'notification_urls': [], # Apprise URL list
|
'notification_urls': [], # Apprise URL list
|
||||||
'pager_size': 50,
|
'pager_size': 50,
|
||||||
|
|||||||
@@ -309,6 +309,9 @@ def process_notification(n_object: NotificationContextData, datastore):
|
|||||||
if not isinstance(n_object, NotificationContextData):
|
if not isinstance(n_object, NotificationContextData):
|
||||||
raise TypeError(f"Expected NotificationContextData, got {type(n_object)}")
|
raise TypeError(f"Expected NotificationContextData, got {type(n_object)}")
|
||||||
|
|
||||||
|
if not n_object.get('notification_urls'):
|
||||||
|
return None
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if n_object.get('notification_timestamp'):
|
if n_object.get('notification_timestamp'):
|
||||||
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
|
logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
|
||||||
@@ -348,16 +351,15 @@ def process_notification(n_object: NotificationContextData, datastore):
|
|||||||
apprise.plugins.N_MGR.remove('discord')
|
apprise.plugins.N_MGR.remove('discord')
|
||||||
apprise.plugins.N_MGR.add(NotifyDiscordCustom, schemas='discord')
|
apprise.plugins.N_MGR.add(NotifyDiscordCustom, schemas='discord')
|
||||||
|
|
||||||
if not n_object.get('notification_urls'):
|
# Should always be false for 'text' mode or its too hard to read, otherwise it's a setting (for html style).
|
||||||
return None
|
word_diff_enable = requested_output_format_original == 'text' or (
|
||||||
|
n_object.get('notification_html_word_diff_enabled', True) and requested_output_format_original.startswith('html'))
|
||||||
|
|
||||||
n_object.update(add_rendered_diff_to_notification_vars(
|
n_object.update(add_rendered_diff_to_notification_vars(
|
||||||
notification_scan_text=n_object.get('notification_body', '')+n_object.get('notification_title', ''),
|
notification_scan_text=n_object.get('notification_body', '')+n_object.get('notification_title', ''),
|
||||||
current_snapshot=n_object.get('current_snapshot'),
|
current_snapshot=n_object.get('current_snapshot'),
|
||||||
prev_snapshot=n_object.get('prev_snapshot'),
|
prev_snapshot=n_object.get('prev_snapshot'),
|
||||||
# Should always be false for 'text' mode or its too hard to read
|
word_diff=word_diff_enable
|
||||||
# But otherwise, this could be some setting
|
|
||||||
word_diff=False if requested_output_format_original == 'text' else True,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ class NotificationService:
|
|||||||
if n_object.get('notification_format') == USE_SYSTEM_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')
|
n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format')
|
||||||
|
|
||||||
|
n_object['notification_html_word_diff_enabled'] = self.datastore.data['settings']['application'].get('notification_html_word_diff_enabled', True)
|
||||||
|
|
||||||
triggered_text = ''
|
triggered_text = ''
|
||||||
if len(trigger_text):
|
if len(trigger_text):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
{% from '_helpers.html' import render_field %}
|
{% from '_helpers.html' import render_field, render_checkbox_field %}
|
||||||
|
|
||||||
{% macro show_token_placeholders(extra_notification_token_placeholder_info, suffix="") %}
|
{% macro show_token_placeholders(extra_notification_token_placeholder_info, suffix="") %}
|
||||||
|
|
||||||
@@ -8,9 +8,7 @@
|
|||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
Body for all notifications ‐ You can use <a target="newwindow" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
|
Body for all notifications ‐ You can use <a target="newwindow" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
|
||||||
</span><br>
|
</span><br>
|
||||||
<div data-target="#notification-tokens-info{{ suffix }}" class="toggle-show pure-button button-tag button-xsmall">Show
|
<div data-target="#notification-tokens-info{{ suffix }}" class="toggle-show pure-button button-tag button-xsmall">Show extra help and tokens</div>
|
||||||
token/placeholders
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-controls" style="display: none;" id="notification-tokens-info{{ suffix }}">
|
<div class="pure-controls" style="display: none;" id="notification-tokens-info{{ suffix }}">
|
||||||
<table class="pure-table" id="token-table">
|
<table class="pure-table" id="token-table">
|
||||||
@@ -105,11 +103,30 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<br>
|
||||||
<span class="pure-form-message-inline">
|
<div class="pure-form-message-inline">
|
||||||
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
|
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
|
||||||
For example, an addition or removal could be perceived as a change in some cases. <a target="newwindow" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
For example, an addition or removal could be perceived as a change in some cases. <a target="newwindow" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
||||||
</span>
|
</div>
|
||||||
|
<br><br>
|
||||||
|
<div class="pure-form-message-inline">
|
||||||
|
<ul>
|
||||||
|
<li><span class="pure-form-message-inline">
|
||||||
|
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>
|
||||||
|
</span></li>
|
||||||
|
<li><span class="pure-form-message-inline">
|
||||||
|
URL encoding, use <strong>|urlencode</strong>, for example - <code>gets://hook-website.com/test.php?title={{ '{{ watch_title|urlencode }}' }}</code>
|
||||||
|
</span></li>
|
||||||
|
<li><span class="pure-form-message-inline">
|
||||||
|
Regular-expression replace, use <strong>|regex_replace</strong>, for example - <code>{{ "{{ \"hello world 123\" | regex_replace('[0-9]+', 'no-more-numbers') }}" }}</code>
|
||||||
|
</span></li>
|
||||||
|
<li><span class="pure-form-message-inline">
|
||||||
|
For a complete reference of all Jinja2 built-in filters, users can refer to the <a
|
||||||
|
href="https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters">https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters</a>
|
||||||
|
</span></li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@@ -151,28 +168,11 @@
|
|||||||
{{ render_field(form.notification_title, class="m-d notification-title", placeholder=settings_application['notification_title']) }}
|
{{ render_field(form.notification_title, class="m-d notification-title", placeholder=settings_application['notification_title']) }}
|
||||||
<span class="pure-form-message-inline">Title for all notifications</span>
|
<span class="pure-form-message-inline">Title for all notifications</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div>
|
||||||
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
|
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
|
||||||
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info) }}
|
{{ show_token_placeholders(extra_notification_token_placeholder_info=extra_notification_token_placeholder_info) }}
|
||||||
<div class="pure-form-message-inline">
|
|
||||||
<ul>
|
|
||||||
<li><span class="pure-form-message-inline">
|
|
||||||
For JSON payloads, use <strong>|tojson</strong> without quotes for automatic escaping, for example - <code>{ "name": {{ '{{ watch_title|tojson }}' }} }</code>
|
|
||||||
</span></li>
|
|
||||||
<li><span class="pure-form-message-inline">
|
|
||||||
URL encoding, use <strong>|urlencode</strong>, for example - <code>gets://hook-website.com/test.php?title={{ '{{ watch_title|urlencode }}' }}</code>
|
|
||||||
</span></li>
|
|
||||||
<li><span class="pure-form-message-inline">
|
|
||||||
Regular-expression replace, use <strong>|regex_replace</strong>, for example - <code>{{ "{{ \"hello world 123\" | regex_replace('[0-9]+', 'no-more-numbers') }}" }}</code>
|
|
||||||
</span></li>
|
|
||||||
<li><span class="pure-form-message-inline">
|
|
||||||
For a complete reference of all Jinja2 built-in filters, users can refer to the <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters">https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters</a>
|
|
||||||
</span></li>
|
|
||||||
</ul>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div>
|
||||||
{{ render_field(form.notification_format , class="notification-format") }}
|
{{ render_field(form.notification_format , class="notification-format") }}
|
||||||
<span class="pure-form-message-inline">Format for all notifications</span>
|
<span class="pure-form-message-inline">Format for all notifications</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -532,7 +532,7 @@ def test_single_send_test_notification_on_watch(client, live_server, measure_mem
|
|||||||
assert 'Current snapshot: Example text: example test' in x
|
assert 'Current snapshot: Example text: example test' in x
|
||||||
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
os.unlink(os.path.join(datastore_path, "notification.txt"))
|
||||||
|
|
||||||
def _test_color_notifications(client, notification_body_token, datastore_path):
|
def _test_color_notifications(client, notification_body_token, datastore_path, word_diff_enabled = True):
|
||||||
|
|
||||||
set_original_response(datastore_path=datastore_path)
|
set_original_response(datastore_path=datastore_path)
|
||||||
|
|
||||||
@@ -551,6 +551,7 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
|
|||||||
"application-minutes_between_check": 180,
|
"application-minutes_between_check": 180,
|
||||||
"application-notification_body": notification_body_token,
|
"application-notification_body": notification_body_token,
|
||||||
"application-notification_format": "htmlcolor",
|
"application-notification_format": "htmlcolor",
|
||||||
|
"application-notification_html_word_diff_enabled": 'y' if word_diff_enabled else '',
|
||||||
"application-notification_urls": test_notification_url,
|
"application-notification_urls": test_notification_url,
|
||||||
"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
|
"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
|
||||||
},
|
},
|
||||||
@@ -559,17 +560,13 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
|
|||||||
assert b'Settings updated' in res.data
|
assert b'Settings updated' in res.data
|
||||||
|
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
|
||||||
url_for("ui.ui_views.form_quick_watch_add"),
|
res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||||
data={"url": test_url, "tags": 'nice one'},
|
assert b'Queued 1 watch for rechecking.' in res.data
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert b"Watch added" in res.data
|
|
||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
extras='XXX ' if word_diff_enabled else ''
|
||||||
set_modified_response(datastore_path=datastore_path)
|
set_modified_response(datastore_path=datastore_path, extras=extras)
|
||||||
|
|
||||||
|
|
||||||
res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
res = client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||||
@@ -579,9 +576,13 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
|
|||||||
wait_for_notification_endpoint_output(datastore_path=datastore_path)
|
wait_for_notification_endpoint_output(datastore_path=datastore_path)
|
||||||
|
|
||||||
with open(os.path.join(datastore_path, "notification.txt"), 'r') as f:
|
with open(os.path.join(datastore_path, "notification.txt"), 'r') as f:
|
||||||
x = f.read()
|
contents = f.read()
|
||||||
s = f'<span style="{HTML_CHANGED_STYLE}" role="note" aria-label="Changed text" title="Changed text">Which is across multiple lines</span><br>'
|
s = f'<span style="{HTML_CHANGED_STYLE}" role="note" aria-label="Changed text" title="Changed text">Which is across multiple lines</span><br>'
|
||||||
assert s in x
|
assert s in contents
|
||||||
|
if word_diff_enabled:
|
||||||
|
assert '>XXX</span>' in contents
|
||||||
|
else:
|
||||||
|
assert '>XXX</span>' not in contents
|
||||||
|
|
||||||
client.get(
|
client.get(
|
||||||
url_for("ui.form_delete", uuid="all"),
|
url_for("ui.form_delete", uuid="all"),
|
||||||
@@ -590,6 +591,12 @@ def _test_color_notifications(client, notification_body_token, datastore_path):
|
|||||||
|
|
||||||
# Just checks the format of the colour notifications was correct
|
# Just checks the format of the colour notifications was correct
|
||||||
def test_html_color_notifications(client, live_server, measure_memory_usage, datastore_path):
|
def test_html_color_notifications(client, live_server, measure_memory_usage, datastore_path):
|
||||||
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path)
|
# Word-level diff only triggers when difflib.SequenceMatcher identifies a single-line to single-line replacement.
|
||||||
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path)
|
# If you have multiple changed lines close together, you need at least 1 unchanged content line (not empty) between them to
|
||||||
|
# prevent them from being grouped into a multi-line replacement that falls back to line-level diff.
|
||||||
|
|
||||||
|
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path, word_diff_enabled = True)
|
||||||
|
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path, word_diff_enabled = True)
|
||||||
|
|
||||||
|
_test_color_notifications(client, '{{diff}}',datastore_path=datastore_path, word_diff_enabled = False)
|
||||||
|
_test_color_notifications(client, '{{diff_full}}',datastore_path=datastore_path, word_diff_enabled = False)
|
||||||
@@ -7,7 +7,7 @@ import logging
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
def set_original_response(datastore_path, extra_title=''):
|
def set_original_response(datastore_path, extra_title='', extras=''):
|
||||||
test_return_data = f"""<html>
|
test_return_data = f"""<html>
|
||||||
<head><title>head title{extra_title}</title></head>
|
<head><title>head title{extra_title}</title></head>
|
||||||
<body>
|
<body>
|
||||||
@@ -15,6 +15,9 @@ def set_original_response(datastore_path, extra_title=''):
|
|||||||
<p>Which is across multiple lines</p>
|
<p>Which is across multiple lines</p>
|
||||||
<br>
|
<br>
|
||||||
So let's see what happens. <br>
|
So let's see what happens. <br>
|
||||||
|
with more text that helps word-diff if needed<br>
|
||||||
|
and more text that helps word-diff if needed<br>
|
||||||
|
and even more text {extras}that helps word-diff if needed<br>
|
||||||
<span class="foobar-detection" style='display:none'></span>
|
<span class="foobar-detection" style='display:none'></span>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -24,14 +27,17 @@ def set_original_response(datastore_path, extra_title=''):
|
|||||||
f.write(test_return_data)
|
f.write(test_return_data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_modified_response(datastore_path):
|
def set_modified_response(datastore_path, extras=''):
|
||||||
test_return_data = """<html>
|
test_return_data =f"""<html>
|
||||||
<head><title>modified head title</title></head>
|
<head><title>modified head title</title></head>
|
||||||
<body>
|
<body>
|
||||||
Some initial text<br>
|
Some initial text<br>
|
||||||
<p>which has this one new line</p>
|
<p>which has this one new line</p>
|
||||||
<br>
|
<br>
|
||||||
So let's see what happens. <br>
|
So let's see what happens. <br>
|
||||||
|
with more text that helps word-diff if needed<br>
|
||||||
|
and more text that helps word-diff if needed<br>
|
||||||
|
and even more text {extras}that helps word-diff if needed<br>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -92,8 +98,8 @@ def wait_for_notification_endpoint_output(datastore_path):
|
|||||||
#@todo - could check the apprise object directly instead of looking for this file
|
#@todo - could check the apprise object directly instead of looking for this file
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
notification_file = os.path.join(datastore_path, "notification.txt")
|
notification_file = os.path.join(datastore_path, "notification.txt")
|
||||||
for i in range(1, 20):
|
for i in range(1, 100):
|
||||||
time.sleep(1)
|
time.sleep(0.3)
|
||||||
if isfile(notification_file):
|
if isfile(notification_file):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user