mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-29 01:27:30 +00:00
Work on link display logic for add-watch-ui
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, make_response
|
||||
from flask import Blueprint, render_template, request, jsonify, make_response, flash, redirect, url_for
|
||||
from flask_babel import gettext
|
||||
from loguru import logger
|
||||
|
||||
from changedetectionio import forms
|
||||
@@ -17,6 +18,14 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_config
|
||||
from changedetectionio.llm.ui_strings import LLM_INTENT_WATCH_PLACEHOLDER
|
||||
|
||||
# Same gate that hides the sidebar link (sidebar-nav.html): with no browser that can
|
||||
# render a preview there is nothing for the visual selector to work on, so bounce
|
||||
# direct navigation rather than serving a page whose only outcome is an error.
|
||||
if not browser_config.has_visual_browser(datastore):
|
||||
flash(gettext("Adding a watch with a browser needs an interactive browser "
|
||||
"(screenshots + element data) - none is configured."), 'error')
|
||||
return redirect(url_for('watchlist.index'))
|
||||
|
||||
form = forms.quickWatchForm(None)
|
||||
llm_configured = bool(_get_llm_config(datastore))
|
||||
|
||||
|
||||
@@ -83,6 +83,15 @@ def list_visual_browser_choices(datastore):
|
||||
return choices
|
||||
|
||||
|
||||
def has_visual_browser(datastore):
|
||||
"""True when at least one installed browser can render a live preview.
|
||||
|
||||
Gates the whole Add-Watch page (sidebar link + the route itself): without one there
|
||||
is nothing for the visual selector to work on, so the page can only fail.
|
||||
"""
|
||||
return bool(list_visual_browser_choices(datastore))
|
||||
|
||||
|
||||
def default_visual_browser(datastore):
|
||||
"""Which browser the Add-Watch page should start on.
|
||||
|
||||
|
||||
@@ -611,6 +611,14 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
from changedetectionio.llm.evaluator import is_llm_features_disabled
|
||||
return dict(llm_features_disabled=is_llm_features_disabled())
|
||||
|
||||
@app.context_processor
|
||||
def inject_has_visual_browser():
|
||||
# Whether any installed content fetcher can render the Add-Watch live preview -
|
||||
# sidebar-nav.html hides the Add-Watch link without one. Same capability lookup the
|
||||
# page's browser picker and /snapshot use, so they can't disagree.
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
return dict(has_visual_browser=browser_config.has_visual_browser(datastore))
|
||||
|
||||
# Set up a request hook to check authentication for all routes
|
||||
@app.before_request
|
||||
def check_authentication():
|
||||
|
||||
@@ -387,8 +387,8 @@ body.watch-selection-active #checkbox-operations {
|
||||
transparent
|
||||
);
|
||||
outline-offset: 1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
border-radius: var(--common-round-border);
|
||||
}
|
||||
|
||||
// Reserved for future use
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{# Add-Watch needs a browser that can render a live preview (see the add_watch_ui
|
||||
blueprint's browser_config) - without one the page can only fail, so don't offer it #}
|
||||
{%- if has_visual_browser -%}
|
||||
<li class="action-sidebar-li">
|
||||
<a href="{{ url_for('add_watch_ui.add_watch_ui_index') }}"
|
||||
class="action-sidebar-item {% if ep.startswith('add_watch_ui.') %}active{% endif %}"
|
||||
@@ -37,6 +40,7 @@
|
||||
<span class="action-label">{{ _('Add a page watch') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif -%}
|
||||
<li class="action-sidebar-li">
|
||||
<a href="{{ url_for('watchlist.index') }}"
|
||||
class="action-sidebar-item {% if ep == 'watchlist.index' or ep.startswith('ui.ui_views') or ep.startswith('ui.ui_edit') %}active{% endif %}"
|
||||
|
||||
@@ -22,6 +22,13 @@ def _llm_markers_absent(body: bytes, where: str = ''):
|
||||
def test_llm_features_disabled_hides_ui(client, live_server, monkeypatch):
|
||||
monkeypatch.setenv('LLM_FEATURES_DISABLED', 'true')
|
||||
|
||||
# The Add-Watch page (checked at the end) is gated on having a browser that can render a
|
||||
# live preview, which a plain test container doesn't - pretend one is installed so this
|
||||
# test stays about LLM surfaces, not about which fetchers happen to be available.
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
|
||||
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
|
||||
|
||||
# Sanity: helper reports the env var is in effect
|
||||
from changedetectionio.llm.evaluator import is_llm_features_disabled, get_llm_config
|
||||
assert is_llm_features_disabled() is True
|
||||
@@ -74,6 +81,9 @@ def test_llm_features_enabled_by_default(client, live_server, monkeypatch):
|
||||
# With an LLM configured, the Add-watch page offers both ways of narrowing a watch,
|
||||
# so "Select by element" goes back to being an opt-in checkbox.
|
||||
monkeypatch.setenv('LLM_MODEL', 'gemini/gemini-2.5-flash')
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
|
||||
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
|
||||
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
|
||||
assert res.status_code == 200
|
||||
assert b'name="llm_intent"' in res.data
|
||||
|
||||
@@ -17,6 +17,43 @@ def _datastore(client):
|
||||
return client.application.config.get('DATASTORE')
|
||||
|
||||
|
||||
def test_hidden_without_a_live_preview_browser(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""No capable browser -> the sidebar link is hidden and the route bounces.
|
||||
|
||||
One seam is patched deliberately: has_visual_browser() (page + sidebar gate) and
|
||||
default_visual_browser() both resolve through list_visual_browser_choices(), so the
|
||||
gate can't disagree with what the picker would have offered.
|
||||
"""
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'list_visual_browser_choices', lambda datastore: [])
|
||||
|
||||
# Sidebar link is not rendered
|
||||
res = client.get(url_for('watchlist.index'))
|
||||
assert url_for('add_watch_ui.add_watch_ui_index').encode() not in res.data
|
||||
|
||||
# Direct navigation is bounced back to the watch list
|
||||
res = client.get(url_for('add_watch_ui.add_watch_ui_index'), follow_redirects=True)
|
||||
assert b'name="fetch_backend"' not in res.data
|
||||
assert b'needs an interactive browser' in res.data
|
||||
|
||||
|
||||
def test_shown_when_a_live_preview_browser_exists(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""A capable browser -> the sidebar link is offered and the page serves its picker."""
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
|
||||
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
|
||||
|
||||
res = client.get(url_for('watchlist.index'))
|
||||
assert url_for('add_watch_ui.add_watch_ui_index').encode() in res.data
|
||||
|
||||
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
|
||||
assert res.status_code == 200
|
||||
assert b'name="fetch_backend"' in res.data
|
||||
assert b'value="html_webdriver"' in res.data
|
||||
# The system default resolves to html_requests (no preview), so a real browser is preselected
|
||||
assert b'checked' in res.data
|
||||
|
||||
|
||||
def test_browser_picker_lists_only_live_preview_capable(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""Capable browsers are offered; the plain HTTP client never is."""
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
@@ -37,20 +74,6 @@ def test_browser_picker_lists_only_live_preview_capable(client, live_server, mea
|
||||
assert b'disabled' in res.data
|
||||
|
||||
|
||||
def test_browser_picker_disables_incapable_system_default(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""With nothing capable at all, 'system' is still shown (disabled) rather than vanishing."""
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'is_visual_capable', lambda name, datastore: False)
|
||||
|
||||
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
|
||||
assert res.status_code == 200
|
||||
assert b'name="fetch_backend"' in res.data
|
||||
assert b'value="system"' in res.data
|
||||
assert b'disabled' in res.data
|
||||
# ...and nothing else is offered
|
||||
assert b'value="html_' not in res.data
|
||||
|
||||
|
||||
def test_snapshot_refuses_browser_that_cannot_preview(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""/snapshot won't spend a fetch on a browser that produces no screenshot."""
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_zh_Hant_TW_timeago_integration():
|
||||
assert '天前' in result_3d, f"Expected '天前' in '{result_3d}'"
|
||||
|
||||
|
||||
def test_language_switching(client, live_server, measure_memory_usage, datastore_path):
|
||||
def test_language_switching(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
|
||||
"""
|
||||
Test that the language switching functionality works correctly.
|
||||
|
||||
@@ -77,6 +77,14 @@ def test_language_switching(client, live_server, measure_memory_usage, datastore
|
||||
3. Switch back to English and verify English text appears
|
||||
"""
|
||||
|
||||
# The Add-Watch page is only served when a browser that can render a live preview is
|
||||
# installed (it is the page this test reads translated processor labels off). A plain
|
||||
# test container has none - without PLAYWRIGHT_DRIVER_URL html_webdriver is Selenium,
|
||||
# which cannot - so pretend one exists rather than assert against a redirect.
|
||||
from changedetectionio.blueprint.add_watch_ui import browser_config
|
||||
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
|
||||
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
|
||||
|
||||
# Establish session cookie
|
||||
client.get(url_for("add_watch_ui.add_watch_ui_index"), follow_redirects=True)
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -14,6 +14,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -19,6 +19,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.8\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-08-25 14:13+0200\n"
|
||||
"POT-Creation-Date: 2026-08-25 17:06+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,6 +17,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -14,6 +14,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -19,6 +19,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -17,6 +17,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -19,6 +19,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -17,6 +17,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
@@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.18.0\n"
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/__init__.py
|
||||
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
|
||||
#, python-format
|
||||
msgid "System settings default (%(browser)s)"
|
||||
|
||||
Reference in New Issue
Block a user