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 <title>"), # actual page title <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 }}") }} </div> {% endif %} <a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a> - {{ render_common_settings_form(form, emailprefix, settings_application, extra_notification_token_placeholder_info) }} </div> </fieldset> @@ -317,33 +316,36 @@ Math: {{ 1 + 1 }}") }} {% if watch['processor'] == 'text_json_diff' %} <div class="tab-pane-inner" id="conditions"> - <!-- Grid Header --> - <table> - <thead> - <td>In Value</td> - <td>Operator</td> - <td>Value</td> - <td></td> + <div class="pure-control-group"> + <a id="conditions-delete-all" class="pure-button button-secondary button-xsmall" >Reset all conditions</a> + </div> - </thead> - <tbody> - <!-- Rule Rows (Dynamic Content) --> - - {% for rule in form.conditions %} - <tr> - <td>{{ rule.field() }}</td> - - <td>{{ rule.operator() }}</td> - <td>{{ rule.value() }}</td> - - <td> - <button type="button" onclick="addRuleRow()">AND +</button> - </td> - </tr> - {% endfor %} - </tbody> - </table> + <div class="pure-control-group"> + <!-- Grid Header --> + {{ render_field(form.conditions_match_logic) }} + <table class="pure-table"> + <thead> + <td>In Value</td> + <td>Operator</td> + <td>Value</td> + <td></td> + </thead> + <tbody> + <!-- Rule Rows (Dynamic Content) --> + {% for rule in form.conditions %} + <tr> + <td>{{ rule.field() }}</td> + <td>{{ rule.operator() }}</td> + <td>{{ rule.value() }}</td> + <td> + <button type="button" onclick="addRuleRow()">ADD +</button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> </div> <div class="tab-pane-inner" id="filters-and-triggers"> <span id="activate-text-preview" class="pure-button pure-button-primary button-xsmall">Activate preview</span>