diff --git a/changedetectionio/blueprint/tags/__init__.py b/changedetectionio/blueprint/tags/__init__.py
index 4b6d40ca8..8a7035177 100644
--- a/changedetectionio/blueprint/tags/__init__.py
+++ b/changedetectionio/blueprint/tags/__init__.py
@@ -106,6 +106,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
form = group_restock_settings_form(formdata=request.form if request.method == 'POST' else None,
data=default,
+ extra_notification_tokens=datastore.get_unique_notification_tokens_available()
)
template_args = {
@@ -161,6 +162,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
form = group_restock_settings_form(formdata=request.form if request.method == 'POST' else None,
data=default,
+ extra_notification_tokens=datastore.get_unique_notification_tokens_available()
)
# @todo subclass form so validation works
#if not form.validate():
diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py
index d5e361580..ffb82f01c 100644
--- a/changedetectionio/flask_app.py
+++ b/changedetectionio/flask_app.py
@@ -696,8 +696,9 @@ def changedetection_app(config=None, datastore_o=None):
form_class = forms.processor_text_json_diff_form
form = form_class(formdata=request.form if request.method == 'POST' else None,
- data=default
- )
+ data=default,
+ extra_notification_tokens=default.extra_notification_token_values()
+ )
# For the form widget tag UUID back to "string name" for the field
form.tags.datastore = datastore
@@ -878,7 +879,8 @@ def changedetection_app(config=None, datastore_o=None):
# Don't use form.data on POST so that it doesnt overrid the checkbox status from the POST status
form = forms.globalSettingsForm(formdata=request.form if request.method == 'POST' else None,
- data=default
+ data=default,
+ extra_notification_tokens=datastore.get_unique_notification_tokens_available()
)
# Remove the last option 'System default'
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 2cefae90d..b0b19f990 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -231,9 +231,6 @@ class ValidateJinja2Template(object):
"""
Validates that a {token} is from a valid set
"""
- def __init__(self, message=None):
- self.message = message
-
def __call__(self, form, field):
from changedetectionio import notification
@@ -248,6 +245,10 @@ class ValidateJinja2Template(object):
try:
jinja2_env = ImmutableSandboxedEnvironment(loader=BaseLoader)
jinja2_env.globals.update(notification.valid_tokens)
+ # Extra validation tokens provided on the form_class(... extra_tokens={}) setup
+ if hasattr(field, 'extra_notification_tokens'):
+ jinja2_env.globals.update(field.extra_notification_tokens)
+
jinja2_env.from_string(joined_data).render()
except TemplateSyntaxError as e:
raise ValidationError(f"This is not a valid Jinja2 template: {e}") from e
@@ -422,6 +423,12 @@ class quickWatchForm(Form):
class commonSettingsForm(Form):
from . import processors
+ def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
+ super().__init__(formdata, obj, prefix, data, meta, **kwargs)
+ self.notification_body.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
+ self.notification_title.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
+ self.notification_urls.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
+
extract_title_as_title = BooleanField('Extract
from document and use as watch title', default=False)
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()])
@@ -429,8 +436,8 @@ class commonSettingsForm(Form):
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")
- webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1,
- message="Should contain one or more seconds")])
+ webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1, message="Should contain one or more seconds")])
+
class importForm(Form):
from . import processors
@@ -590,6 +597,11 @@ class globalSettingsForm(Form):
# Define these as FormFields/"sub forms", this way it matches the JSON storage
# datastore.data['settings']['application']..
# datastore.data['settings']['requests']..
+ def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
+ super().__init__(formdata, obj, prefix, data, meta, **kwargs)
+ self.application.notification_body.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
+ self.application.notification_title.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
+ self.application.notification_urls.extra_notification_tokens = kwargs.get('extra_notification_tokens', {})
requests = FormField(globalSettingsRequestForm)
application = FormField(globalSettingsApplicationForm)
diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py
index e4697f381..d3167bf90 100644
--- a/changedetectionio/model/Watch.py
+++ b/changedetectionio/model/Watch.py
@@ -432,6 +432,17 @@ class model(watch_base):
def toggle_mute(self):
self['notification_muted'] ^= True
+ def extra_notification_token_values(self):
+ # Used for providing extra tokens
+ # return {'widget': 555}
+ return {}
+
+ def extra_notification_token_placeholder_info(self):
+ # Used for providing extra tokens
+ # return [('widget', "Get widget amounts")]
+ return []
+
+
def extract_regex_from_all_history(self, regex):
import csv
import re
diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py
index 41285ce45..ae01f0014 100644
--- a/changedetectionio/notification.py
+++ b/changedetectionio/notification.py
@@ -272,19 +272,18 @@ def create_notification_parameters(n_object, datastore):
tokens.update(
{
'base_url': base_url,
- 'current_snapshot': n_object.get('current_snapshot', ''),
- 'diff': n_object.get('diff', ''), # Null default in the case we use a test
- 'diff_added': n_object.get('diff_added', ''), # Null default in the case we use a test
- 'diff_full': n_object.get('diff_full', ''), # Null default in the case we use a test
- 'diff_patch': n_object.get('diff_patch', ''), # Null default in the case we use a test
- 'diff_removed': n_object.get('diff_removed', ''), # Null default in the case we use a test
'diff_url': diff_url,
'preview_url': preview_url,
- 'triggered_text': n_object.get('triggered_text', ''),
'watch_tag': watch_tag if watch_tag is not None else '',
'watch_title': watch_title if watch_title is not None else '',
'watch_url': watch_url,
'watch_uuid': uuid,
})
+ # n_object will contain diff, diff_added etc etc
+ tokens.update(n_object)
+
+ if uuid:
+ tokens.update(datastore.data['watching'].get(uuid).extra_notification_token_values())
+
return tokens
diff --git a/changedetectionio/processors/restock_diff/__init__.py b/changedetectionio/processors/restock_diff/__init__.py
index 3f0be03b2..e3331f93b 100644
--- a/changedetectionio/processors/restock_diff/__init__.py
+++ b/changedetectionio/processors/restock_diff/__init__.py
@@ -68,3 +68,16 @@ class Watch(BaseWatch):
super().clear_watch()
self.update({'restock': Restock()})
+ def extra_notification_token_values(self):
+ values = super().extra_notification_token_values()
+ values['restock'] = self.get('restock', {})
+ return values
+
+ def extra_notification_token_placeholder_info(self):
+ values = super().extra_notification_token_values()
+
+ values.append(('restock.price', "Price detected"))
+ values.append(('restock.original_price', "Original price at first check"))
+
+ return values
+
diff --git a/changedetectionio/store.py b/changedetectionio/store.py
index 0d88f8b27..2621bdd1d 100644
--- a/changedetectionio/store.py
+++ b/changedetectionio/store.py
@@ -631,6 +631,19 @@ class ChangeDetectionStore:
return True
return False
+ def get_unique_notification_tokens_available(self):
+ # Ask each type of watch if they have any extra notification token to add to the validation
+ extra_notification_tokens = {}
+ watch_processors_checked = set()
+
+ for watch_uuid, watch in self.__data['watching'].items():
+ processor = watch.get('processor')
+ if processor not in watch_processors_checked:
+ extra_notification_tokens.update(watch.extra_notification_token_values())
+ watch_processors_checked.add(processor)
+
+ return extra_notification_tokens
+
def get_updates_available(self):
import inspect
updates_available = []
diff --git a/changedetectionio/tests/test_restock_itemprop.py b/changedetectionio/tests/test_restock_itemprop.py
index d1af83a09..853184d42 100644
--- a/changedetectionio/tests/test_restock_itemprop.py
+++ b/changedetectionio/tests/test_restock_itemprop.py
@@ -1,8 +1,10 @@
#!/usr/bin/python3
+import os
import time
from flask import url_for
from .util import live_server_setup, wait_for_all_checks, extract_UUID_from_client
+from ..notification import default_notification_format
instock_props = [
# LD+JSON with non-standard list of 'type' https://github.com/dgtlmoon/changedetection.io/issues/1833
@@ -305,6 +307,64 @@ def test_itemprop_percent_threshold(client, live_server):
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data
+
+
+def test_change_with_notification_values(client, live_server):
+ #live_server_setup(live_server)
+
+ if os.path.isfile("test-datastore/notification.txt"):
+ os.unlink("test-datastore/notification.txt")
+
+ test_url = url_for('test_endpoint', _external=True)
+ set_original_response(props_markup=instock_props[0], price='960.45')
+
+ notification_url = url_for('test_notification_endpoint', _external=True).replace('http', 'json')
+
+ ######################
+ # You must add a type of 'restock_diff' for its tokens to register as valid in the global settings
+ client.post(
+ url_for("form_quick_watch_add"),
+ data={"url": test_url, "tags": 'restock tests', 'processor': 'restock_diff'},
+ follow_redirects=True
+ )
+
+ # A change in price, should trigger a change by default
+ wait_for_all_checks(client)
+ #####################
+ # Set this up for when we remove the notification from the watch, it should fallback with these details
+ res = client.post(
+ url_for("settings_page"),
+ data={"application-notification_urls": notification_url,
+ "application-notification_title": "title new price {{restock.price}}",
+ "application-notification_body": "new price {{restock.price}}",
+ "application-notification_format": default_notification_format,
+ "requests-time_between_check-minutes": 180,
+ 'application-fetch_backend': "html_requests"},
+ follow_redirects=True
+ )
+
+ # check tag accepts without error
+
+ # Check the watches in these modes add the tokens for validating
+ assert b"A variable or function is not defined" not in res.data
+
+ assert b"Settings updated." in res.data
+
+
+ set_original_response(props_markup=instock_props[0], price='960.45')
+ # A change in price, should trigger a change by default
+ set_original_response(props_markup=instock_props[0], price='1950.45')
+ client.get(url_for("form_watch_checknow"))
+ wait_for_all_checks(client)
+ time.sleep(3)
+ assert os.path.isfile("test-datastore/notification.txt"), "Notification received"
+ with open("test-datastore/notification.txt", 'r') as f:
+ notification = f.read()
+ assert "new price 1950.45" in notification
+ assert "title new price 1950.45" in notification
+
+
+
def test_data_sanity(client, live_server):
#live_server_setup(live_server)
diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py
index 104db24c5..1b2207f84 100644
--- a/changedetectionio/update_worker.py
+++ b/changedetectionio/update_worker.py
@@ -81,6 +81,9 @@ class update_worker(threading.Thread):
'uuid': watch.get('uuid') if watch else None,
'watch_url': watch.get('url') if watch else None,
})
+
+ n_object.update(watch.extra_notification_token_values())
+
logger.trace(f"Main rendered notification placeholders (diff_added etc) calculated in {time.time()-now:.3f}s")
logger.debug("Queued notification for sending")
notification_q.put(n_object)