Compare commits

...

3 Commits

Author SHA1 Message Date
dgtlmoon
e6572efecc Re #3337 - Various fixes for 'Extract Data' 2025-07-28 17:38:42 +02:00
dgtlmoon
011fa3540e 0.50.7
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-07-15 13:28:15 +02:00
dgtlmoon
c3c3671f8b UI - Set default favicon, handle default 'not set' for new/updated installations
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-07-14 18:37:41 +02:00
6 changed files with 40 additions and 17 deletions

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki # Read more https://github.com/dgtlmoon/changedetection.io/wiki
__version__ = '0.50.6' __version__ = '0.50.7'
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError

View File

@@ -93,12 +93,15 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
return redirect(url_for('watchlist.index')) return redirect(url_for('watchlist.index'))
# For submission of requesting an extract # For submission of requesting an extract
extract_form = forms.extractDataForm(request.form) extract_form = forms.extractDataForm(formdata=request.form,
data={'extract_regex': request.form.get('extract_regex', '')}
)
if not extract_form.validate(): if not extract_form.validate():
flash("An error occurred, please see below.", "error") flash("An error occurred, please see below.", "error")
return _render_diff_template(uuid, extract_form)
else: else:
extract_regex = request.form.get('extract_regex').strip() extract_regex = request.form.get('extract_regex', '').strip()
output = watch.extract_regex_from_all_history(extract_regex) output = watch.extract_regex_from_all_history(extract_regex)
if output: if output:
watch_dir = os.path.join(datastore.datastore_path, uuid) watch_dir = os.path.join(datastore.datastore_path, uuid)
@@ -109,12 +112,11 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
response.headers['Expires'] = "0" response.headers['Expires'] = "0"
return response return response
flash('Nothing matches that RegEx', 'error') flash('No matches found while scanning all of the watch history for that RegEx.', 'error')
redirect(url_for('ui_views.diff_history_page', uuid=uuid) + '#extract') return redirect(url_for('ui.ui_views.diff_history_page', uuid=uuid) + '#extract')
@views_blueprint.route("/diff/<string:uuid>", methods=['GET']) def _render_diff_template(uuid, extract_form=None):
@login_optionally_required """Helper function to render the diff template with all required data"""
def diff_history_page(uuid):
from changedetectionio import forms from changedetectionio import forms
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
@@ -128,8 +130,11 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
flash("No history found for the specified link, bad link?", "error") flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('watchlist.index')) return redirect(url_for('watchlist.index'))
# For submission of requesting an extract # Use provided form or create a new one
extract_form = forms.extractDataForm(request.form) if extract_form is None:
extract_form = forms.extractDataForm(formdata=request.form,
data={'extract_regex': request.form.get('extract_regex', '')}
)
history = watch.history history = watch.history
dates = list(history.keys()) dates = list(history.keys())
@@ -170,7 +175,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
datastore.set_last_viewed(uuid, time.time()) datastore.set_last_viewed(uuid, time.time())
output = render_template("diff.html", return render_template("diff.html",
current_diff_url=watch['url'], current_diff_url=watch['url'],
from_version=str(from_version), from_version=str(from_version),
to_version=str(to_version), to_version=str(to_version),
@@ -193,7 +198,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
watch_a=watch watch_a=watch
) )
return output @views_blueprint.route("/diff/<string:uuid>", methods=['GET'])
@login_optionally_required
def diff_history_page(uuid):
return _render_diff_template(uuid)
@views_blueprint.route("/form/add/quickwatch", methods=['POST']) @views_blueprint.route("/form/add/quickwatch", methods=['POST'])
@login_optionally_required @login_optionally_required

View File

@@ -81,10 +81,11 @@ document.addEventListener('DOMContentLoaded', function() {
{%- if any_has_restock_price_processor -%} {%- if any_has_restock_price_processor -%}
{%- set cols_required = cols_required + 1 -%} {%- set cols_required = cols_required + 1 -%}
{%- endif -%} {%- endif -%}
{%- set ui_settings = datastore.data['settings']['application']['ui'] -%}
<div id="watch-table-wrapper"> <div id="watch-table-wrapper">
{%- set table_classes = [ {%- set table_classes = [
'favicon-enabled' if datastore.data['settings']['application']['ui'].get('favicons_enabled') else 'favicon-not-enabled', 'favicon-enabled' if 'favicons_enabled' not in ui_settings or ui_settings['favicons_enabled'] else 'favicon-not-enabled',
] -%} ] -%}
<table class="pure-table pure-table-striped watch-table {{ table_classes | reject('equalto', '') | join(' ') }}"> <table class="pure-table pure-table-striped watch-table {{ table_classes | reject('equalto', '') | join(' ') }}">
<thead> <thead>
@@ -147,7 +148,7 @@ document.addEventListener('DOMContentLoaded', function() {
<td class="title-col inline"> <td class="title-col inline">
<div class="flex-wrapper"> <div class="flex-wrapper">
{% if datastore.data['settings']['application']['ui'].get('favicons_enabled') %} {% if 'favicons_enabled' not in ui_settings or ui_settings['favicons_enabled'] %}
<div>{# A page might have hundreds of these images, set IMG options for lazy loading, don't set SRC if we dont have it so it doesnt fetch the placeholder' #} <div>{# A page might have hundreds of these images, set IMG options for lazy loading, don't set SRC if we dont have it so it doesnt fetch the placeholder' #}
<img alt="Favicon thumbnail" class="favicon" loading="lazy" decoding="async" fetchpriority="low" {% if favicon %} src="{{url_for('static_content', group='favicon', filename=watch.uuid)}}" {% else %} src='data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="7.087" height="7.087" viewBox="0 0 7.087 7.087"%3E%3Ccircle cx="3.543" cy="3.543" r="3.279" stroke="%23e1e1e1" stroke-width="0.45" fill="none" opacity="0.74"/%3E%3C/svg%3E' {% endif %} /> <img alt="Favicon thumbnail" class="favicon" loading="lazy" decoding="async" fetchpriority="low" {% if favicon %} src="{{url_for('static_content', group='favicon', filename=watch.uuid)}}" {% else %} src='data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="7.087" height="7.087" viewBox="0 0 7.087 7.087"%3E%3Ccircle cx="3.543" cy="3.543" r="3.279" stroke="%23e1e1e1" stroke-width="0.45" fill="none" opacity="0.74"/%3E%3C/svg%3E' {% endif %} />
</div> </div>

View File

@@ -396,6 +396,19 @@ def validate_url(test_url):
# This should be wtforms.validators. # This should be wtforms.validators.
raise ValidationError('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format') raise ValidationError('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format')
class ValidateSinglePythonRegexString(object):
def __init__(self, message=None):
self.message = message
def __call__(self, form, field):
try:
re.compile(field.data)
except re.error:
message = field.gettext('RegEx \'%s\' is not a valid regular expression.')
raise ValidationError(message % (field.data))
class ValidateListRegex(object): class ValidateListRegex(object):
""" """
Validates that anything that looks like a regex passes as a regex Validates that anything that looks like a regex passes as a regex
@@ -414,6 +427,7 @@ class ValidateListRegex(object):
message = field.gettext('RegEx \'%s\' is not a valid regular expression.') message = field.gettext('RegEx \'%s\' is not a valid regular expression.')
raise ValidationError(message % (line)) raise ValidationError(message % (line))
class ValidateCSSJSONXPATHInput(object): class ValidateCSSJSONXPATHInput(object):
""" """
Filter validation Filter validation
@@ -791,5 +805,5 @@ class globalSettingsForm(Form):
class extractDataForm(Form): class extractDataForm(Form):
extract_regex = StringField('RegEx to extract', validators=[validators.Length(min=1, message="Needs a RegEx")]) extract_regex = StringField('RegEx to extract', validators=[validators.DataRequired(), ValidateSinglePythonRegexString()])
extract_submit_button = SubmitField('Extract as CSV', render_kw={"class": "pure-button pure-button-primary"}) extract_submit_button = SubmitField('Extract as CSV', render_kw={"class": "pure-button pure-button-primary"})

View File

@@ -639,7 +639,7 @@ class model(watch_base):
if res: if res:
if not csv_writer: if not csv_writer:
# A file on the disk can be transferred much faster via flask than a string reply # A file on the disk can be transferred much faster via flask than a string reply
csv_output_filename = 'report.csv' csv_output_filename = f"report-{self.get('uuid')}.csv"
f = open(os.path.join(self.watch_data_dir, csv_output_filename), 'w') f = open(os.path.join(self.watch_data_dir, csv_output_filename), 'w')
# @todo some headers in the future # @todo some headers in the future
#fieldnames = ['Epoch seconds', 'Date'] #fieldnames = ['Epoch seconds', 'Date']

View File

@@ -46,7 +46,7 @@ def test_check_extract_text_from_diff(client, live_server, measure_memory_usage)
follow_redirects=False follow_redirects=False
) )
assert b'Nothing matches that RegEx' not in res.data assert b'No matches found while scanning all of the watch history for that RegEx.' not in res.data
assert res.content_type == 'text/csv' assert res.content_type == 'text/csv'
# Read the csv reply as stringio # Read the csv reply as stringio