mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-26 08:06:39 +00:00
WIP
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from flask_login import current_user
|
||||
from json_logic.builtins import BUILTINS
|
||||
|
||||
from .exceptions import EmptyConditionRuleRowNotUsable
|
||||
@@ -5,6 +7,7 @@ from .pluggy_interface import plugin_manager # Import the pluggy plugin manager
|
||||
from . import default_plugin
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
# List of all supported JSON Logic operators
|
||||
operator_choices = [
|
||||
@@ -131,6 +134,76 @@ def execute_ruleset_against_all_plugins(current_watch_uuid: str, application_dat
|
||||
|
||||
return result
|
||||
|
||||
# Flask Blueprint Definition
|
||||
def construct_blueprint(datastore):
|
||||
from changedetectionio.flask_app import login_optionally_required
|
||||
|
||||
conditions_blueprint = Blueprint('conditions', __name__, template_folder="templates")
|
||||
|
||||
@conditions_blueprint.route("/<string:watch_uuid>/verify-condition-single-rule", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def verify_condition_single_rule(watch_uuid):
|
||||
"""Verify a single condition rule against the current snapshot"""
|
||||
|
||||
# Get the watch data
|
||||
watch = datastore.data['watching'].get(watch_uuid)
|
||||
if not watch:
|
||||
return jsonify({'status': 'error', 'message': 'Watch not found'}), 404
|
||||
|
||||
# Get the rule data from the request
|
||||
rule_data = request.json
|
||||
if not rule_data:
|
||||
return jsonify({'status': 'error', 'message': 'No rule data provided'}), 400
|
||||
|
||||
# Create ephemeral data with the current snapshot
|
||||
ephemeral_data = {}
|
||||
|
||||
# Get the current snapshot if available
|
||||
if watch.history_n and watch.get_last_fetched_text_before_filters():
|
||||
ephemeral_data['text'] = watch.get_last_fetched_text_before_filters()
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'No snapshot available for verification. Please fetch content first.'
|
||||
}), 400
|
||||
|
||||
# Test the rule
|
||||
result = False
|
||||
try:
|
||||
# Create a temporary structure with just this rule
|
||||
temp_watch_data = {
|
||||
"conditions": [rule_data],
|
||||
"conditions_match_logic": "ALL" # Single rule, so use ALL
|
||||
}
|
||||
|
||||
# Create a temporary application data structure
|
||||
temp_app_data = {
|
||||
'watching': {
|
||||
watch_uuid: temp_watch_data
|
||||
}
|
||||
}
|
||||
|
||||
# Execute the rule against the current snapshot
|
||||
result = execute_ruleset_against_all_plugins(
|
||||
current_watch_uuid=watch_uuid,
|
||||
application_datastruct=temp_app_data,
|
||||
ephemeral_data=ephemeral_data
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'result': result,
|
||||
'message': 'Condition passes' if result else 'Condition does not pass'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Error verifying condition: {str(e)}'
|
||||
}), 500
|
||||
|
||||
return conditions_blueprint
|
||||
|
||||
# Load plugins dynamically
|
||||
for plugin in plugin_manager.get_plugins():
|
||||
new_ops = plugin.register_operators()
|
||||
|
||||
@@ -1684,6 +1684,9 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
import changedetectionio.blueprint.backups as backups
|
||||
app.register_blueprint(backups.construct_blueprint(datastore), url_prefix='/backups')
|
||||
|
||||
import changedetectionio.conditions as conditions
|
||||
app.register_blueprint(conditions.construct_blueprint(datastore), url_prefix='/conditions')
|
||||
|
||||
# @todo handle ctrl break
|
||||
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
|
||||
threading.Thread(target=notification_runner).start()
|
||||
|
||||
@@ -2,7 +2,7 @@ $(document).ready(function () {
|
||||
// Function to set up button event handlers
|
||||
function setupButtonHandlers() {
|
||||
// Unbind existing handlers first to prevent duplicates
|
||||
$(".addRuleRow, .removeRuleRow").off("click");
|
||||
$(".addRuleRow, .removeRuleRow, .verifyRuleRow").off("click");
|
||||
|
||||
// Add row button handler
|
||||
$(".addRuleRow").on("click", function(e) {
|
||||
@@ -34,12 +34,78 @@ $(document).ready(function () {
|
||||
reindexRules();
|
||||
}
|
||||
});
|
||||
|
||||
// Verify rule button handler
|
||||
$(".verifyRuleRow").on("click", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let row = $(this).closest("tr");
|
||||
let field = row.find("select[name$='field']").val();
|
||||
let operator = row.find("select[name$='operator']").val();
|
||||
let value = row.find("input[name$='value']").val();
|
||||
|
||||
// Validate that all fields are filled
|
||||
if (!field || field === "None" || !operator || operator === "None" || !value) {
|
||||
alert("Please fill in all fields (Field, Operator, and Value) before verifying.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the watch UUID from the URL
|
||||
const url = window.location.pathname;
|
||||
const uuidMatch = url.match(/\/edit\/([^\/]+)/);
|
||||
if (!uuidMatch || !uuidMatch[1]) {
|
||||
alert("Could not determine the watch UUID. Please save your changes first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const watchUuid = uuidMatch[1];
|
||||
|
||||
// Create a rule object
|
||||
const rule = {
|
||||
field: field,
|
||||
operator: operator,
|
||||
value: value
|
||||
};
|
||||
|
||||
// Show a spinner or some indication that verification is in progress
|
||||
const $button = $(this);
|
||||
const originalHTML = $button.html();
|
||||
$button.html("⌛").prop("disabled", true);
|
||||
|
||||
// Send the request to verify the rule
|
||||
$.ajax({
|
||||
url: `/conditions/${watchUuid}/verify-condition-single-rule`,
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(rule),
|
||||
success: function(response) {
|
||||
if (response.status === "success") {
|
||||
if (response.result) {
|
||||
alert("✅ Condition PASSES verification against current snapshot!");
|
||||
} else {
|
||||
alert("❌ Condition FAILS verification against current snapshot.");
|
||||
}
|
||||
} else {
|
||||
alert("Error: " + response.message);
|
||||
}
|
||||
$button.html(originalHTML).prop("disabled", false);
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMsg = "Error verifying condition.";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMsg = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMsg);
|
||||
$button.html(originalHTML).prop("disabled", false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to reindex form elements and re-setup event handlers
|
||||
function reindexRules() {
|
||||
// Unbind all button handlers first
|
||||
$(".addRuleRow, .removeRuleRow").off("click");
|
||||
$(".addRuleRow, .removeRuleRow, .verifyRuleRow").off("click");
|
||||
|
||||
// Reindex all form elements
|
||||
$("#rulesTable tbody tr").each(function(index) {
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<td>
|
||||
<button type="button" class="addRuleRow">+</button>
|
||||
<button type="button" class="removeRuleRow">-</button>
|
||||
<button type="button" class="verifyRuleRow" title="Verify this rule against current snapshot">✓</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -286,9 +286,24 @@ Math: {{ 1 + 1 }}") }}
|
||||
{% if watch['processor'] == 'text_json_diff' %}
|
||||
|
||||
<div class="tab-pane-inner" id="conditions">
|
||||
<style>
|
||||
.verifyRuleRow {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
.verifyRuleRow:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
</style>
|
||||
<div class="pure-control-group">
|
||||
{{ render_field(form.conditions_match_logic) }}
|
||||
{{ render_fieldlist_of_formfields_as_table(form.conditions) }}
|
||||
<div class="pure-form-message-inline">
|
||||
Use the verify (✓) button to test if a condition passes against the current snapshot.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane-inner" id="filters-and-triggers">
|
||||
|
||||
@@ -43,91 +43,12 @@ def set_number_out_of_range_response(number="150"):
|
||||
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||
f.write(test_return_data)
|
||||
|
||||
def test_setup(live_server):
|
||||
live_server_setup(live_server)
|
||||
|
||||
def test_number_within_range_condition(client, live_server, measure_memory_usage):
|
||||
set_original_response()
|
||||
# live_server_setup(live_server)
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
|
||||
# Add our URL to the import page
|
||||
res = client.post(
|
||||
url_for("import_page"),
|
||||
data={"urls": test_url},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"1 Imported" in res.data
|
||||
|
||||
# Configure the watch with two conditions:
|
||||
# 1. The extracted number should be >= 20 and <= 100
|
||||
# 2. The first digit of the number should be in the page filtered text
|
||||
res = client.post(
|
||||
url_for("edit_page", uuid="first"),
|
||||
data={
|
||||
"url": test_url,
|
||||
"fetch_backend": "html_requests",
|
||||
"include_filters": ".number-container",
|
||||
"title": "Condition Test",
|
||||
"conditions_match_logic": "ALL", # ALL = AND logic
|
||||
|
||||
"conditions-0-operator": "in",
|
||||
"conditions-0-field": "page_filtered_text",
|
||||
"conditions-0-value": "5",
|
||||
|
||||
"conditions-1-operator": ">=",
|
||||
"conditions-1-field": "extracted_number",
|
||||
"conditions-1-value": "20",
|
||||
|
||||
"conditions-2-operator": "<=",
|
||||
"conditions-2-field": "extracted_number",
|
||||
"conditions-2-value": "100",
|
||||
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"Updated watch." in res.data
|
||||
|
||||
# Trigger initial check
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Set current view state - no unviewed
|
||||
client.get(url_for("diff_history_page", uuid="first"))
|
||||
|
||||
# Change that stays within the conditions (number is in range and text contains first digit)
|
||||
set_number_in_range_response("75")
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Should be marked as having changes since all conditions are met
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' in res.data
|
||||
assert b'Condition Test' in res.data
|
||||
|
||||
# Now change to value that's outside the range (but still contains the first digit in text)
|
||||
set_number_out_of_range_response("150")
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Should NOT be marked as having changes since one condition is not met
|
||||
res = client.get(url_for("index"))
|
||||
assert b'unviewed' not in res.data
|
||||
assert b'Condition Test' in res.data
|
||||
|
||||
# Check the diff history shows the change from the earlier check (75)
|
||||
res = client.get(url_for("diff_history_page", uuid="first"))
|
||||
assert b'Current value: 75' in res.data
|
||||
|
||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||
assert b'Deleted' in res.data
|
||||
|
||||
def test_conditions_with_text_and_number(client, live_server, measure_memory_usage):
|
||||
"""Test that both text and number conditions work together with AND logic."""
|
||||
|
||||
set_original_response("50")
|
||||
#live_server_setup(live_server)
|
||||
live_server_setup(live_server)
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
|
||||
@@ -175,7 +96,7 @@ def test_conditions_with_text_and_number(client, live_server, measure_memory_usa
|
||||
|
||||
# Case 1: The number is in range but the first digit changed (now 7 instead of 5)
|
||||
# This should NOT trigger a change notification since not all conditions are met
|
||||
set_number_in_range_response("75")
|
||||
set_number_in_range_response("70.5")
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
@@ -187,7 +108,7 @@ def test_conditions_with_text_and_number(client, live_server, measure_memory_usa
|
||||
# Case 2: Change with one condition violated
|
||||
# Number out of range (150) but contains '5'
|
||||
client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
||||
set_number_out_of_range_response("150")
|
||||
set_number_out_of_range_response("150.5")
|
||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user