diff --git a/changedetectionio/blueprint/watchlist/__init__.py b/changedetectionio/blueprint/watchlist/__init__.py index 87969b9d..3d13f4ab 100644 --- a/changedetectionio/blueprint/watchlist/__init__.py +++ b/changedetectionio/blueprint/watchlist/__init__.py @@ -13,7 +13,50 @@ from changedetectionio.auth_decorator import login_optionally_required def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMetaData): watchlist_blueprint = Blueprint('watchlist', __name__, template_folder="templates") - + + # --- Watchlist filtering, shared by the rendered list (index) and the + # "/uuids" endpoint (used by the client "select all matching" feature) so the + # two can never drift out of sync. --- + def _resolve_active_tag_uuid(active_tag_req): + if not active_tag_req: + return None + for uuid, tag in datastore.data['settings']['application'].get('tags', {}).items(): + if active_tag_req == tag.get('title', '').lower().strip() or active_tag_req == uuid: + return uuid + return None + + def _list_filters_from_args(args, active_tag_uuid): + return { + 'with_errors': args.get('with_errors') == "1", + 'unread_only': args.get('unread') == "1", + 'processor': args.get('processor', '').strip(), + 'tag_uuid': active_tag_uuid, + 'search_q': args.get('q').strip().lower() if args.get('q') else False, + } + + # Status/tag/processor filters (everything except the text search). + def _watch_passes_prefilter(watch, f): + if f['with_errors'] and not watch.get('last_error'): + return False + if f['unread_only'] and (watch.viewed or watch.last_changed == 0): + return False + if f['tag_uuid'] and f['tag_uuid'] not in watch['tags']: + return False + if f['processor'] and watch.get('processor') != f['processor']: + return False + return True + + def _watch_passes_search(watch, f): + search_q = f['search_q'] + if not search_q: + return True + if (watch.get('title') and search_q in watch.get('title').lower()) or search_q in watch.get('url', '').lower(): + return True + if watch.get('last_error') and search_q in watch.get('last_error').lower(): + return True + return False + + @watchlist_blueprint.route("/", methods=['GET']) @login_optionally_required def index(): @@ -45,32 +88,16 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe # Sort by last_changed and add the uuid which is usually the key.. sorted_watches = [] - with_errors = request.args.get('with_errors') == "1" - unread_only = request.args.get('unread') == "1" active_processor = request.args.get('processor', '').strip() errored_count = 0 - search_q = request.args.get('q').strip().lower() if request.args.get('q') else False + list_filters = _list_filters_from_args(request.args, active_tag_uuid) for uuid, watch in datastore.data['watching'].items(): - if with_errors and not watch.get('last_error'): + if not _watch_passes_prefilter(watch, list_filters): continue - - if unread_only and (watch.viewed or watch.last_changed == 0) : - continue - - if active_tag_uuid and not active_tag_uuid in watch['tags']: - continue - - if active_processor and watch.get('processor') != active_processor: - continue + # errored_count reflects the tag/processor/status-filtered set (pre-search) if watch.get('last_error'): errored_count += 1 - - if search_q: - if (watch.get('title') and search_q in watch.get('title').lower()) or search_q in watch.get('url', '').lower(): - sorted_watches.append(watch) - elif watch.get('last_error') and search_q in watch.get('last_error').lower(): - sorted_watches.append(watch) - else: + if _watch_passes_search(watch, list_filters): sorted_watches.append(watch) form = forms.quickWatchForm(request.form) @@ -149,5 +176,23 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe resp.set_cookie('order', request.args.get('order')) return resp - + + @watchlist_blueprint.route("/uuids", methods=['GET']) + @login_optionally_required + def uuids(): + """All watch UUIDs matching the current filter (tag/search/status/processor). + + Backs the client "select all matching" feature: the watchlist page only + renders one page of rows, so to select across pages the browser fetches the + full matching id list from here and holds it in its selection store. + """ + from flask import jsonify + active_tag_uuid = _resolve_active_tag_uuid(request.args.get('tag', '').lower().strip()) + list_filters = _list_filters_from_args(request.args, active_tag_uuid) + matching = [ + uuid for uuid, watch in datastore.data['watching'].items() + if _watch_passes_prefilter(watch, list_filters) and _watch_passes_search(watch, list_filters) + ] + return jsonify({'uuids': matching}) + return watchlist_blueprint \ No newline at end of file diff --git a/changedetectionio/blueprint/watchlist/templates/watch-overview.html b/changedetectionio/blueprint/watchlist/templates/watch-overview.html index 3746c68d..c3a49fd5 100644 --- a/changedetectionio/blueprint/watchlist/templates/watch-overview.html +++ b/changedetectionio/blueprint/watchlist/templates/watch-overview.html @@ -11,6 +11,14 @@ +