diff --git a/changedetectionio/conditions/__init__.py b/changedetectionio/conditions/__init__.py
index abb05933..7a205f1b 100644
--- a/changedetectionio/conditions/__init__.py
+++ b/changedetectionio/conditions/__init__.py
@@ -7,6 +7,7 @@ import re
# List of all supported JSON Logic operators
operator_choices = [
+ (None, "Choose one"),
(">", "Greater Than"),
("<", "Less Than"),
(">=", "Greater Than or Equal To"),
@@ -18,36 +19,12 @@ operator_choices = [
("contains_regex", "Text Matches Regex"),
("!contains_regex", "Text Does NOT Match Regex"),
("changed > minutes", "Changed more than X minutes ago"),
-# ("watch_uuid_unviewed_change", "Watch UUID had an unviewed change"), #('if'? )
-# ("watch_uuid_not_unviewed_change", "Watch UUID NOT had an unviewed change") #('if'? )
-# ("watch_uuid_changed", "Watch UUID had unviewed change"),
-# ("watch_uuid_not_changed", "Watch UUID did NOT have unviewed change"),
-# ("!!", "Is Truthy"),
-# ("!", "Is Falsy"),
-# ("and", "All Conditions Must Be True"),
-# ("or", "At Least One Condition Must Be True"),
-# ("max", "Maximum of Values"),
-# ("min", "Minimum of Values"),
-# ("+", "Addition"),
-# ("-", "Subtraction"),
-# ("*", "Multiplication"),
-# ("/", "Division"),
-# ("%", "Modulo"),
-# ("log", "Logarithm"),
-# ("if", "Conditional If-Else")
]
# Fields available in the rules
field_choices = [
- ("extracted_number", "Extracted Number"),
- ("page_filtered_text", "Page text After Filters"),
- ("page_title", "Page Title"), # actual page title
- ("watch_uuid", "Watch UUID"),
- #("watch_history_length", "History Length"), # Would never equate
- ("watch_history", "All Watch Text History"),
- ("watch_check_count", "Watch Check Count"),
- ("watch_uuid", "Other Watch by UUID"), # (Maybe this is 'if' ??)
- #("requests_get", "Web GET requests (https://..)")
+ (None, "Choose one"),
+
]
diff --git a/changedetectionio/conditions/default_plugin.py b/changedetectionio/conditions/default_plugin.py
index 2215c6b5..a143df8d 100644
--- a/changedetectionio/conditions/default_plugin.py
+++ b/changedetectionio/conditions/default_plugin.py
@@ -5,10 +5,10 @@ hookimpl = pluggy.HookimplMarker("conditions")
@hookimpl
def register_operators():
def starts_with(_, text, prefix):
- return text.lower().startswith(prefix.lower())
+ return text.lower().strip().startswith(prefix.lower())
def ends_with(_, text, suffix):
- return text.lower().endswith(suffix.lower())
+ return text.lower().strip().endswith(suffix.lower())
return {
"starts_with": starts_with,
@@ -25,6 +25,9 @@ def register_operator_choices():
@hookimpl
def register_field_choices():
return [
+ ("extracted_number", "Automatically extracted number"),
("meta_description", "Meta Description"),
("meta_keywords", "Meta Keywords"),
- ]
\ No newline at end of file
+ ("page_filtered_text", "Page text after 'Filters & Triggers'"),
+ ("page_title", "Page "), # actual page title
+ ]
diff --git a/changedetectionio/conditions/pluggy_interface.py b/changedetectionio/conditions/pluggy_interface.py
index 16a4948e..cf30207f 100644
--- a/changedetectionio/conditions/pluggy_interface.py
+++ b/changedetectionio/conditions/pluggy_interface.py
@@ -1,8 +1,11 @@
import pluggy
+from . import default_plugin # Import the default plugin
-# Define `pluggy` hookspecs (Specifications for Plugins)
-hookspec = pluggy.HookspecMarker("conditions")
-hookimpl = pluggy.HookimplMarker("conditions")
+# ✅ Ensure that the namespace in HookspecMarker matches PluginManager
+PLUGIN_NAMESPACE = "conditions"
+
+hookspec = pluggy.HookspecMarker(PLUGIN_NAMESPACE)
+hookimpl = pluggy.HookimplMarker(PLUGIN_NAMESPACE)
class ConditionsSpec:
@@ -24,9 +27,14 @@ class ConditionsSpec:
pass
-# ✅ Set up `pluggy` Plugin Manager
-plugin_manager = pluggy.PluginManager("conditions")
+# ✅ Set up Pluggy Plugin Manager
+plugin_manager = pluggy.PluginManager(PLUGIN_NAMESPACE)
+
+# ✅ Register hookspecs (Ensures they are detected)
plugin_manager.add_hookspecs(ConditionsSpec)
-# Discover installed plugins
-plugin_manager.load_setuptools_entrypoints("conditions")
\ No newline at end of file
+# ✅ Register built-in plugins manually
+plugin_manager.register(default_plugin, "default_plugin")
+
+# ✅ Discover installed plugins from external packages (if any)
+plugin_manager.load_setuptools_entrypoints(PLUGIN_NAMESPACE)
diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py
index 6930a79c..a88df476 100644
--- a/changedetectionio/flask_app.py
+++ b/changedetectionio/flask_app.py
@@ -758,25 +758,6 @@ def changedetection_app(config=None, datastore_o=None):
for p in datastore.proxy_list:
form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label'])))
- # Example JSON Rule
- DEFAULT_RULE = {
- "and": [
- {">": [{"var": "extracted_number"}, 5000]},
- {"<": [{"var": "extracted_number"}, 80000]},
- {"in": ["rock", {"var": "page_text"}]}
- ]
- }
- form.conditions.pop_entry() # Remove the default empty row
- for condition in DEFAULT_RULE["and"]:
- operator, values = list(condition.items())[0]
- field = values[0]["var"] if isinstance(values[0], dict) else values[1]["var"]
- value = values[1] if isinstance(values[1], (str, int)) else values[0]
-
- form.conditions.append_entry({
- "operator": operator,
- "field": field,
- "value": value
- })
if request.method == 'POST' and form.validate():
@@ -812,14 +793,6 @@ def changedetection_app(config=None, datastore_o=None):
extra_update_obj['filter_text_replaced'] = True
extra_update_obj['filter_text_removed'] = True
- # Convert form input into JSON Logic format
- extra_update_obj["conditions"] = {
- "and": [
- {c.operator.data: [{"var": c.field.data}, c.value.data]}
- for c in getattr(form, "conditions", []) or []
- ]
- } if getattr(form, "conditions", None) else {}
-
# Because wtforms doesn't support accessing other data in process_ , but we convert the CSV list of tags back to a list of UUIDs
tag_uuids = []
if form.data.get('tags'):
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 118679f4..3e9fecd3 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -306,7 +306,10 @@ class ValidateAppRiseServers(object):
def __call__(self, form, field):
import apprise
apobj = apprise.Apprise()
+
# so that the custom endpoints are registered
+ from .apprise_asset import asset
+
for server_url in field.data:
url = server_url.strip()
if url.startswith("#"):
@@ -512,6 +515,9 @@ class quickWatchForm(Form):
# Condition Rule Form (for each rule row)
class ConditionForm(FlaskForm):
+ from changedetectionio.conditions import plugin_manager
+
+ # ✅ Ensure Plugins Are Loaded BEFORE Importing Choices
from changedetectionio.conditions import operator_choices, field_choices
operator = SelectField(
@@ -613,6 +619,7 @@ class processor_text_json_diff_form(commonSettingsForm):
notification_muted = BooleanField('Notifications Muted / Off', default=False)
notification_screenshot = BooleanField('Attach screenshot to notification (where possible)', default=False)
+ conditions_match_logic = RadioField(u'Match', choices=[('ALL', 'Match all of the following'),('ANY', 'Match any of the following')], default='ALL')
conditions = FieldList(FormField(ConditionForm), min_entries=1) # Add rule logic here
diff --git a/changedetectionio/static/styles/scss/parts/_edit.scss b/changedetectionio/static/styles/scss/parts/_edit.scss
new file mode 100644
index 00000000..ea01b692
--- /dev/null
+++ b/changedetectionio/static/styles/scss/parts/_edit.scss
@@ -0,0 +1,9 @@
+ul#conditions_match_logic {
+ list-style: none;
+ input, label, li {
+ display: inline-block;
+ }
+ li {
+ padding-right: 1em;
+ }
+}
diff --git a/changedetectionio/static/styles/scss/styles.scss b/changedetectionio/static/styles/scss/styles.scss
index 4c698088..8e87dc5e 100644
--- a/changedetectionio/static/styles/scss/styles.scss
+++ b/changedetectionio/static/styles/scss/styles.scss
@@ -13,6 +13,7 @@
@import "parts/_menu";
@import "parts/_love";
@import "parts/preview_text_filter";
+@import "parts/_edit";
body {
color: var(--color-text);
diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css
index 2e8a6407..104fe012 100644
--- a/changedetectionio/static/styles/styles.css
+++ b/changedetectionio/static/styles/styles.css
@@ -523,6 +523,13 @@ body.preview-text-enabled {
z-index: 3;
box-shadow: 1px 1px 4px var(--color-shadow-jump); }
+ul#conditions_match_logic {
+ list-style: none; }
+ ul#conditions_match_logic input, ul#conditions_match_logic label, ul#conditions_match_logic li {
+ display: inline-block; }
+ ul#conditions_match_logic li {
+ padding-right: 1em; }
+
body {
color: var(--color-text);
background: var(--color-background-page);
diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html
index 1d554c54..f52fe9bd 100644
--- a/changedetectionio/templates/edit.html
+++ b/changedetectionio/templates/edit.html
@@ -308,7 +308,6 @@ Math: {{ 1 + 1 }}") }}
{% endif %}
Use system defaults
-
{{ render_common_settings_form(form, emailprefix, settings_application, extra_notification_token_placeholder_info) }}
@@ -317,33 +316,36 @@ Math: {{ 1 + 1 }}") }}
{% if watch['processor'] == 'text_json_diff' %}
-
-
-
- | In Value |
- Operator |
- Value |
- |
+
-
-
-
-
- {% for rule in form.conditions %}
-
- | {{ rule.field() }} |
-
- {{ rule.operator() }} |
- {{ rule.value() }} |
-
-
-
- |
-
- {% endfor %}
-
-
+
+
+ {{ render_field(form.conditions_match_logic) }}
+
+
+ | In Value |
+ Operator |
+ Value |
+ |
+
+
+
+ {% for rule in form.conditions %}
+
+ | {{ rule.field() }} |
+ {{ rule.operator() }} |
+ {{ rule.value() }} |
+
+
+ |
+
+ {% endfor %}
+
+
+
Activate preview