From a3a3ab062299cdf10c9bbba56862ee40db96f432 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 13 Dec 2024 11:21:39 +0100 Subject: [PATCH] Notifcations - Adding "HTML Color" notification format option (#2837) --- changedetectionio/diff.py | 45 ++++++++++------ changedetectionio/notification.py | 4 ++ changedetectionio/tests/test_notification.py | 57 ++++++++++++++++++++ changedetectionio/update_worker.py | 8 ++- 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/changedetectionio/diff.py b/changedetectionio/diff.py index 859abe6a..0d5fb363 100644 --- a/changedetectionio/diff.py +++ b/changedetectionio/diff.py @@ -12,11 +12,12 @@ def customSequenceMatcher( include_removed: bool = True, include_added: bool = True, include_replaced: bool = True, - include_change_type_prefix: bool = True + include_change_type_prefix: bool = True, + html_colour: bool = False ) -> Iterator[List[str]]: """ Compare two sequences and yield differences based on specified parameters. - + Args: before (List[str]): Original sequence after (List[str]): Modified sequence @@ -25,26 +26,33 @@ def customSequenceMatcher( include_added (bool): Include added parts include_replaced (bool): Include replaced parts include_change_type_prefix (bool): Add prefixes to indicate change types - + html_colour (bool): Use HTML background colors for differences + Yields: List[str]: Differences between sequences """ cruncher = difflib.SequenceMatcher(isjunk=lambda x: x in " \t", a=before, b=after) - + for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): if include_equal and tag == 'equal': yield before[alo:ahi] elif include_removed and tag == 'delete': - prefix = "(removed) " if include_change_type_prefix else '' - yield [f"{prefix}{line}" for line in same_slicer(before, alo, ahi)] + if html_colour: + yield [f'{line}' for line in same_slicer(before, alo, ahi)] + else: + yield [f"(removed) {line}" for line in same_slicer(before, alo, ahi)] if include_change_type_prefix else same_slicer(before, alo, ahi) elif include_replaced and tag == 'replace': - prefix_changed = "(changed) " if include_change_type_prefix else '' - prefix_into = "(into) " if include_change_type_prefix else '' - yield [f"{prefix_changed}{line}" for line in same_slicer(before, alo, ahi)] + \ - [f"{prefix_into}{line}" for line in same_slicer(after, blo, bhi)] + if html_colour: + yield [f'{line}' for line in same_slicer(before, alo, ahi)] + \ + [f'{line}' for line in same_slicer(after, blo, bhi)] + else: + yield [f"(changed) {line}" for line in same_slicer(before, alo, ahi)] + \ + [f"(into) {line}" for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(before, alo, ahi) + same_slicer(after, blo, bhi) elif include_added and tag == 'insert': - prefix = "(added) " if include_change_type_prefix else '' - yield [f"{prefix}{line}" for line in same_slicer(after, blo, bhi)] + if html_colour: + yield [f'{line}' for line in same_slicer(after, blo, bhi)] + else: + yield [f"(added) {line}" for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(after, blo, bhi) def render_diff( previous_version_file_contents: str, @@ -55,11 +63,12 @@ def render_diff( include_replaced: bool = True, line_feed_sep: str = "\n", include_change_type_prefix: bool = True, - patch_format: bool = False + patch_format: bool = False, + html_colour: bool = False ) -> str: """ Render the difference between two file contents. - + Args: previous_version_file_contents (str): Original file contents newest_version_file_contents (str): Modified file contents @@ -70,7 +79,8 @@ def render_diff( line_feed_sep (str): Separator for lines in output include_change_type_prefix (bool): Add prefixes to indicate change types patch_format (bool): Use patch format for output - + html_colour (bool): Use HTML background colors for differences + Returns: str: Rendered difference """ @@ -88,10 +98,11 @@ def render_diff( include_removed=include_removed, include_added=include_added, include_replaced=include_replaced, - include_change_type_prefix=include_change_type_prefix + include_change_type_prefix=include_change_type_prefix, + html_colour=html_colour ) def flatten(lst: List[Union[str, List[str]]]) -> str: return line_feed_sep.join(flatten(x) if isinstance(x, list) else x for x in lst) - return flatten(rendered_diff) + return flatten(rendered_diff) \ No newline at end of file diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py index 42dcdce7..e7ca67a3 100644 --- a/changedetectionio/notification.py +++ b/changedetectionio/notification.py @@ -31,6 +31,7 @@ valid_notification_formats = { 'Text': NotifyFormat.TEXT, 'Markdown': NotifyFormat.MARKDOWN, 'HTML': NotifyFormat.HTML, + 'HTML Color': 'htmlcolor', # Used only for editing a watch (not for global) default_notification_format_for_watch: default_notification_format_for_watch } @@ -76,6 +77,9 @@ def process_notification(n_object, datastore): # Get the notification body from datastore n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters) + if n_object.get('notification_format', '').startswith('HTML'): + n_body = n_body.replace("\n", '
') + n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters) url = url.strip() diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index e47693f3..b4871951 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -442,4 +442,61 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage assert b"Error: You must have atleast one watch configured for 'test notification' to work" in res.data +def test_html_color_notifications(client, live_server, measure_memory_usage): + + #live_server_setup(live_server) + + set_original_response() + + if os.path.isfile("test-datastore/notification.txt"): + os.unlink("test-datastore/notification.txt") + + + test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}&+custom-header=123" + + + # otherwise other settings would have already existed from previous tests in this file + res = client.post( + url_for("settings_page"), + data={ + "application-fetch_backend": "html_requests", + "application-minutes_between_check": 180, + "application-notification_body": '{{diff}}', + "application-notification_format": "HTML Color", + "application-notification_urls": test_notification_url, + "application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}", + }, + follow_redirects=True + ) + assert b'Settings updated' in res.data + + test_url = url_for('test_endpoint', _external=True) + res = client.post( + url_for("form_quick_watch_add"), + data={"url": test_url, "tags": 'nice one'}, + follow_redirects=True + ) + + assert b"Watch added" in res.data + + wait_for_all_checks(client) + + set_modified_response() + + + res = client.get(url_for("form_watch_checknow"), follow_redirects=True) + assert b'1 watches queued for rechecking.' in res.data + + wait_for_all_checks(client) + time.sleep(3) + + with open("test-datastore/notification.txt", 'r') as f: + x = f.read() + assert 'Which is across multiple lines' in x + + + client.get( + url_for("form_delete", uuid="all"), + follow_redirects=True + ) diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py index af54eff4..942f87bf 100644 --- a/changedetectionio/update_worker.py +++ b/changedetectionio/update_worker.py @@ -44,11 +44,17 @@ class update_worker(threading.Thread): else: snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." + html_colour_enable = False # HTML needs linebreak, but MarkDown and Text can use a linefeed if n_object.get('notification_format') == 'HTML': line_feed_sep = "
" # Snapshot will be plaintext on the disk, convert to some kind of HTML snapshot_contents = snapshot_contents.replace('\n', line_feed_sep) + elif n_object.get('notification_format') == 'HTML Color': + line_feed_sep = "
" + # Snapshot will be plaintext on the disk, convert to some kind of HTML + snapshot_contents = snapshot_contents.replace('\n', line_feed_sep) + html_colour_enable = True else: line_feed_sep = "\n" @@ -69,7 +75,7 @@ class update_worker(threading.Thread): n_object.update({ 'current_snapshot': snapshot_contents, - 'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep), + 'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), 'diff_added': diff.render_diff(prev_snapshot, current_snapshot, include_removed=False, line_feed_sep=line_feed_sep), 'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep), 'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True),