diff --git a/README.md b/README.md
index a6f9f0ec..6939f4f1 100644
--- a/README.md
+++ b/README.md
@@ -111,7 +111,7 @@ Just some examples
-Now you can also customise your notification content!
+Now you can also customise your notification content and use Jinja2 templating for their title and body!
### JSON API Monitoring
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 99c2c53b..46b6977c 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -133,7 +133,7 @@ class ValidateAppRiseServers(object):
message = field.gettext('\'%s\' is not a valid AppRise URL.' % (server_url))
raise ValidationError(message)
-class ValidateTokensList(object):
+class ValidateJinja2Template(object):
"""
Validates that a {token} is from a valid set
"""
@@ -142,11 +142,22 @@ class ValidateTokensList(object):
def __call__(self, form, field):
from changedetectionio import notification
- regex = re.compile('{.*?}')
- for p in re.findall(regex, field.data):
- if not p.strip('{}') in notification.valid_tokens:
- message = field.gettext('Token \'%s\' is not a valid token.')
- raise ValidationError(message % (p))
+ from jinja2 import Environment, BaseLoader, TemplateSyntaxError
+ from jinja2.meta import find_undeclared_variables
+
+ try:
+ jinja2_env = Environment(loader=BaseLoader)
+ jinja2_env.globals.update(notification.valid_tokens)
+ rendered = jinja2_env.from_string(field.data).render()
+ except TemplateSyntaxError as e:
+ raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
+
+ ast = jinja2_env.parse(field.data)
+ undefined = ", ".join(find_undeclared_variables(ast))
+ if undefined:
+ raise ValidationError(
+ f"The following tokens used in the notification are not valid: {undefined}"
+ )
class ValidateListRegex(object):
"""
@@ -201,8 +212,8 @@ class quickWatchForm(Form):
class commonSettingsForm(Form):
notification_urls = StringListField('Notification URL List', validators=[validators.Optional(), ValidateAppRiseServers()])
- notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {watch_url}', validators=[validators.Optional(), ValidateTokensList()])
- notification_body = TextAreaField('Notification Body', default='{watch_url} had a change.', validators=[validators.Optional(), ValidateTokensList()])
+ notification_title = StringField('Notification Title', default='ChangeDetection.io Notification - {{ watch_url }}', validators=[validators.Optional(), ValidateJinja2Template()])
+ notification_body = TextAreaField('Notification Body', default='{{ watch_url }} had a change.', validators=[validators.Optional(), ValidateJinja2Template()])
trigger_check = BooleanField('Send test notification on save')
fetch_backend = RadioField(u'Fetch Method', choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
extract_title_as_title = BooleanField('Extract
from document and use as watch title', default=False)
diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py
index 0a18fdba..7eb22428 100644
--- a/changedetectionio/notification.py
+++ b/changedetectionio/notification.py
@@ -1,5 +1,5 @@
-import os
import apprise
+from jinja2 import Environment, BaseLoader
valid_tokens = {
'base_url': '',
@@ -24,18 +24,13 @@ def process_notification(n_object, datastore):
print (">> Process Notification: AppRise notifying {}".format(url))
apobj.add(url)
- # Get the notification body from datastore
- n_body = n_object['notification_body']
- n_title = n_object['notification_title']
-
# Insert variables into the notification content
notification_parameters = create_notification_parameters(n_object, datastore)
- for n_k in notification_parameters:
- token = '{' + n_k + '}'
- val = notification_parameters[n_k]
- n_title = n_title.replace(token, val)
- n_body = n_body.replace(token, val)
+ # Get the notification body from datastore
+ jinja2_env = Environment(loader=BaseLoader)
+ n_body = jinja2_env.from_string(n_object['notification_body']).render(**notification_parameters)
+ n_title = jinja2_env.from_string(n_object['notification_title']).render(**notification_parameters)
apobj.notify(
body=n_body,
@@ -61,7 +56,7 @@ def create_notification_parameters(n_object, datastore):
watch_url = n_object['watch_url']
- # Re #148 - Some people have just {base_url} in the body or title, but this may break some notification services
+ # Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services
# like 'Join', so it's always best to atleast set something obvious so that they are not broken.
if base_url == '':
base_url = ""
@@ -85,4 +80,4 @@ def create_notification_parameters(n_object, datastore):
'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else ''
})
- return tokens
\ No newline at end of file
+ return tokens
diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja
index 55573781..1e8dda18 100644
--- a/changedetectionio/templates/_common_fields.jinja
+++ b/changedetectionio/templates/_common_fields.jinja
@@ -26,8 +26,10 @@
- These tokens can be used in the notification body and title to
- customise the notification text.
+ You can use
+ Jinja2
+ templating for the notification title and body.
+ These tokens can be used to customise them.
@@ -38,42 +40,42 @@
- {base_url} |
+ {{ '{{ base_url }}' }} |
The URL of the changedetection.io instance you are running. |
- {watch_url} |
+ {{ '{{ watch_url }}' }} |
The URL being watched. |
- {watch_uuid} |
+ {{ '{{ watch_uuid }}' }} |
The UUID of the watch. |
- {watch_title} |
+ {{ '{{ watch_title }}' }} |
The title of the watch. |
- {watch_tag} |
+ {{ '{{ watch_tag }}' }} |
The tag of the watch. |
- {preview_url} |
+ {{ '{{ preview_url }}' }} |
The URL of the preview page generated by changedetection.io. |
- {diff_url} |
+ {{ '{{ diff_url }}' }} |
The URL of the diff page generated by changedetection.io. |
- {current_snapshot} |
+ {{ '{{ current_snapshot }}' }} |
The current snapshot value, useful when combined with JSON or CSS filters
|
- URLs generated by changedetection.io (such as {diff_url}) require the BASE_URL environment variable set.
+ URLs generated by changedetection.io (such as {{ '{{ diff_url }}' }}) require the BASE_URL environment variable set.
Your BASE_URL var is currently "{{base_url}}"
diff --git a/changedetectionio/templates/settings.html b/changedetectionio/templates/settings.html
index 856f9539..9f2eaf26 100644
--- a/changedetectionio/templates/settings.html
+++ b/changedetectionio/templates/settings.html
@@ -36,7 +36,7 @@
{{ render_field(form.base_url, placeholder="http://yoursite.com:5000/",
class="m-d") }}
- Base URL used for the {base_url} token in notifications, default value is the ENV var 'base_url',
+ Base URL used for the {{ '{{ base_url }}' }} token in notifications, default value is the ENV var 'base_url',
read more here.
diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py
index 95be9a10..f0598915 100644
--- a/changedetectionio/tests/test_notification.py
+++ b/changedetectionio/tests/test_notification.py
@@ -46,15 +46,15 @@ def test_check_notification(client, live_server):
res = client.post(
url_for("edit_page", uuid="first"),
data={"notification_urls": notification_url,
- "notification_title": "New ChangeDetection.io Notification - {watch_url}",
- "notification_body": "BASE URL: {base_url}\n"
- "Watch URL: {watch_url}\n"
- "Watch UUID: {watch_uuid}\n"
- "Watch title: {watch_title}\n"
- "Watch tag: {watch_tag}\n"
- "Preview: {preview_url}\n"
- "Diff URL: {diff_url}\n"
- "Snapshot: {current_snapshot}\n"
+ "notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
+ "notification_body": "BASE URL: {{ base_url }}\n"
+ "Watch URL: {{ watch_url }}\n"
+ "Watch UUID: {{ watch_uuid }}\n"
+ "Watch title: {{ watch_title }}\n"
+ "Watch tag: {{ watch_tag }}\n"
+ "Preview: {{ preview_url }}\n"
+ "Diff URL: {{ diff_url }}\n"
+ "Snapshot: {{ current_snapshot }}\n"
":-)",
"url": test_url,
"tag": "my tag",
@@ -126,7 +126,7 @@ def test_check_notification(client, live_server):
res = client.post(
url_for("settings_page"),
- data={"notification_title": "New ChangeDetection.io Notification - {watch_url}",
+ data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
"notification_urls": "json://foobar.com", #Re #143 should not see that it sent without [test checkbox]
"minutes_between_check": 180,
"fetch_backend": "html_requests",
@@ -179,13 +179,27 @@ def test_check_notification(client, live_server):
# Now adding a wrong token should give us an error
res = client.post(
url_for("settings_page"),
- data={"notification_title": "New ChangeDetection.io Notification - {watch_url}",
- "notification_body": "Rubbish: {rubbish}\n",
+ data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
+ "notification_body": "Rubbish: {{ rubbish }}\n",
"notification_urls": "json://foobar.com",
"minutes_between_check": 180,
"fetch_backend": "html_requests"
},
follow_redirects=True
)
+ assert bytes("The following tokens used in the notification are not valid".encode('utf-8')) in res.data
+
+
+ # And trying to define an invalid Jinja2 template should also throw an error
+ res = client.post(
+ url_for("settings_page"),
+ data={"notification_title": "New ChangeDetection.io Notification - {{ watch_url }}",
+ "notification_body": "Rubbish: {{ rubbish }\n",
+ "notification_urls": "json://foobar.com",
+ "minutes_between_check": 180,
+ "fetch_backend": "html_requests"
+ },
+ follow_redirects=True
+ )
+ assert bytes("This is not a valid Jinja2 template".encode('utf-8')) in res.data
- assert bytes("is not a valid token".encode('utf-8')) in res.data
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index c699fa80..c3c64fa1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,8 +16,9 @@ chardet > 2.3.0
wtforms ~= 2.3.3
jsonpath-ng ~= 1.5.3
-# Notification library
+# Notification libraries
apprise ~= 0.9
+jinja2 ~= 2.11.3
# Pinned version of cryptography otherwise
# ERROR: Could not build wheels for cryptography which use PEP 517 and cannot be installed directly