Skip to content

Commit a3a3ab0

Browse files
authored
Notifcations - Adding "HTML Color" notification format option (#2837)
1 parent c5fe188 commit a3a3ab0

File tree

4 files changed

+96
-18
lines changed

4 files changed

+96
-18
lines changed

changedetectionio/diff.py

+28-17
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ def customSequenceMatcher(
1212
include_removed: bool = True,
1313
include_added: bool = True,
1414
include_replaced: bool = True,
15-
include_change_type_prefix: bool = True
15+
include_change_type_prefix: bool = True,
16+
html_colour: bool = False
1617
) -> Iterator[List[str]]:
1718
"""
1819
Compare two sequences and yield differences based on specified parameters.
19-
20+
2021
Args:
2122
before (List[str]): Original sequence
2223
after (List[str]): Modified sequence
@@ -25,26 +26,33 @@ def customSequenceMatcher(
2526
include_added (bool): Include added parts
2627
include_replaced (bool): Include replaced parts
2728
include_change_type_prefix (bool): Add prefixes to indicate change types
28-
29+
html_colour (bool): Use HTML background colors for differences
30+
2931
Yields:
3032
List[str]: Differences between sequences
3133
"""
3234
cruncher = difflib.SequenceMatcher(isjunk=lambda x: x in " \t", a=before, b=after)
33-
35+
3436
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
3537
if include_equal and tag == 'equal':
3638
yield before[alo:ahi]
3739
elif include_removed and tag == 'delete':
38-
prefix = "(removed) " if include_change_type_prefix else ''
39-
yield [f"{prefix}{line}" for line in same_slicer(before, alo, ahi)]
40+
if html_colour:
41+
yield [f'<span style="background-color: #ffcecb;">{line}</span>' for line in same_slicer(before, alo, ahi)]
42+
else:
43+
yield [f"(removed) {line}" for line in same_slicer(before, alo, ahi)] if include_change_type_prefix else same_slicer(before, alo, ahi)
4044
elif include_replaced and tag == 'replace':
41-
prefix_changed = "(changed) " if include_change_type_prefix else ''
42-
prefix_into = "(into) " if include_change_type_prefix else ''
43-
yield [f"{prefix_changed}{line}" for line in same_slicer(before, alo, ahi)] + \
44-
[f"{prefix_into}{line}" for line in same_slicer(after, blo, bhi)]
45+
if html_colour:
46+
yield [f'<span style="background-color: #ffcecb;">{line}</span>' for line in same_slicer(before, alo, ahi)] + \
47+
[f'<span style="background-color: #dafbe1;">{line}</span>' for line in same_slicer(after, blo, bhi)]
48+
else:
49+
yield [f"(changed) {line}" for line in same_slicer(before, alo, ahi)] + \
50+
[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)
4551
elif include_added and tag == 'insert':
46-
prefix = "(added) " if include_change_type_prefix else ''
47-
yield [f"{prefix}{line}" for line in same_slicer(after, blo, bhi)]
52+
if html_colour:
53+
yield [f'<span style="background-color: #dafbe1;">{line}</span>' for line in same_slicer(after, blo, bhi)]
54+
else:
55+
yield [f"(added) {line}" for line in same_slicer(after, blo, bhi)] if include_change_type_prefix else same_slicer(after, blo, bhi)
4856

4957
def render_diff(
5058
previous_version_file_contents: str,
@@ -55,11 +63,12 @@ def render_diff(
5563
include_replaced: bool = True,
5664
line_feed_sep: str = "\n",
5765
include_change_type_prefix: bool = True,
58-
patch_format: bool = False
66+
patch_format: bool = False,
67+
html_colour: bool = False
5968
) -> str:
6069
"""
6170
Render the difference between two file contents.
62-
71+
6372
Args:
6473
previous_version_file_contents (str): Original file contents
6574
newest_version_file_contents (str): Modified file contents
@@ -70,7 +79,8 @@ def render_diff(
7079
line_feed_sep (str): Separator for lines in output
7180
include_change_type_prefix (bool): Add prefixes to indicate change types
7281
patch_format (bool): Use patch format for output
73-
82+
html_colour (bool): Use HTML background colors for differences
83+
7484
Returns:
7585
str: Rendered difference
7686
"""
@@ -88,10 +98,11 @@ def render_diff(
8898
include_removed=include_removed,
8999
include_added=include_added,
90100
include_replaced=include_replaced,
91-
include_change_type_prefix=include_change_type_prefix
101+
include_change_type_prefix=include_change_type_prefix,
102+
html_colour=html_colour
92103
)
93104

94105
def flatten(lst: List[Union[str, List[str]]]) -> str:
95106
return line_feed_sep.join(flatten(x) if isinstance(x, list) else x for x in lst)
96107

97-
return flatten(rendered_diff)
108+
return flatten(rendered_diff)

changedetectionio/notification.py

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
'Text': NotifyFormat.TEXT,
3232
'Markdown': NotifyFormat.MARKDOWN,
3333
'HTML': NotifyFormat.HTML,
34+
'HTML Color': 'htmlcolor',
3435
# Used only for editing a watch (not for global)
3536
default_notification_format_for_watch: default_notification_format_for_watch
3637
}
@@ -76,6 +77,9 @@ def process_notification(n_object, datastore):
7677

7778
# Get the notification body from datastore
7879
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
80+
if n_object.get('notification_format', '').startswith('HTML'):
81+
n_body = n_body.replace("\n", '<br>')
82+
7983
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
8084

8185
url = url.strip()

changedetectionio/tests/test_notification.py

+57
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,61 @@ def test_global_send_test_notification(client, live_server, measure_memory_usage
442442
assert b"Error: You must have atleast one watch configured for 'test notification' to work" in res.data
443443

444444

445+
def test_html_color_notifications(client, live_server, measure_memory_usage):
446+
447+
#live_server_setup(live_server)
448+
449+
set_original_response()
450+
451+
if os.path.isfile("test-datastore/notification.txt"):
452+
os.unlink("test-datastore/notification.txt")
453+
454+
455+
test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}&+custom-header=123"
456+
457+
458+
# otherwise other settings would have already existed from previous tests in this file
459+
res = client.post(
460+
url_for("settings_page"),
461+
data={
462+
"application-fetch_backend": "html_requests",
463+
"application-minutes_between_check": 180,
464+
"application-notification_body": '{{diff}}',
465+
"application-notification_format": "HTML Color",
466+
"application-notification_urls": test_notification_url,
467+
"application-notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
468+
},
469+
follow_redirects=True
470+
)
471+
assert b'Settings updated' in res.data
472+
473+
test_url = url_for('test_endpoint', _external=True)
474+
res = client.post(
475+
url_for("form_quick_watch_add"),
476+
data={"url": test_url, "tags": 'nice one'},
477+
follow_redirects=True
478+
)
479+
480+
assert b"Watch added" in res.data
481+
482+
wait_for_all_checks(client)
483+
484+
set_modified_response()
485+
486+
487+
res = client.get(url_for("form_watch_checknow"), follow_redirects=True)
488+
assert b'1 watches queued for rechecking.' in res.data
489+
490+
wait_for_all_checks(client)
491+
time.sleep(3)
492+
493+
with open("test-datastore/notification.txt", 'r') as f:
494+
x = f.read()
495+
assert '<span style="background-color: #ffcecb;">Which is across multiple lines' in x
496+
497+
498+
client.get(
499+
url_for("form_delete", uuid="all"),
500+
follow_redirects=True
501+
)
445502

changedetectionio/update_worker.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,17 @@ def queue_notification_for_watch(self, notification_q, n_object, watch):
4444
else:
4545
snapshot_contents = "No snapshot/history available, the watch should fetch atleast once."
4646

47+
html_colour_enable = False
4748
# HTML needs linebreak, but MarkDown and Text can use a linefeed
4849
if n_object.get('notification_format') == 'HTML':
4950
line_feed_sep = "<br>"
5051
# Snapshot will be plaintext on the disk, convert to some kind of HTML
5152
snapshot_contents = snapshot_contents.replace('\n', line_feed_sep)
53+
elif n_object.get('notification_format') == 'HTML Color':
54+
line_feed_sep = "<br>"
55+
# Snapshot will be plaintext on the disk, convert to some kind of HTML
56+
snapshot_contents = snapshot_contents.replace('\n', line_feed_sep)
57+
html_colour_enable = True
5258
else:
5359
line_feed_sep = "\n"
5460

@@ -69,7 +75,7 @@ def queue_notification_for_watch(self, notification_q, n_object, watch):
6975

7076
n_object.update({
7177
'current_snapshot': snapshot_contents,
72-
'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep),
78+
'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, html_colour=html_colour_enable),
7379
'diff_added': diff.render_diff(prev_snapshot, current_snapshot, include_removed=False, line_feed_sep=line_feed_sep),
7480
'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep),
7581
'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True),

0 commit comments

Comments
 (0)