From 20feda86c036a0a3a8dc78961fac2da6ab8b4a2b Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 10 Feb 2026 09:27:10 +0000 Subject: [PATCH] Recap screen fetch cached data on load --- backend/reviews/admin.py | 13 ++--- backend/reviews/templates/reviews-recap.html | 16 ++++++ backend/reviews/tests/test_recap.py | 59 +++++++++++++++++--- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/backend/reviews/admin.py b/backend/reviews/admin.py index ffc35f397e..f09187ca24 100644 --- a/backend/reviews/admin.py +++ b/backend/reviews/admin.py @@ -62,10 +62,7 @@ def with_pct(counts_dict): .order_by("speaker__gender") ) gender_counts = with_pct( - { - item["speaker__gender"] or "unknown": item["count"] - for item in gender_stats - } + {item["speaker__gender"] or "unknown": item["count"] for item in gender_stats} ) level_stats = ( @@ -87,9 +84,7 @@ def with_pct(counts_dict): ) speaker_level_stats = ( - qs.values("speaker_level") - .annotate(count=Count("id")) - .order_by("speaker_level") + qs.values("speaker_level").annotate(count=Count("id")).order_by("speaker_level") ) speaker_level_counts = with_pct( {item["speaker_level"]: item["count"] for item in speaker_level_stats} @@ -455,6 +450,7 @@ def review_recap_compute_analysis_view(self, request, review_session_id): conference = review_session.conference accepted_submissions = list(self._get_accepted_submissions(conference)) force_recompute = request.GET.get("recompute") == "1" + check_only = request.GET.get("check") == "1" from django.core.cache import cache @@ -471,6 +467,9 @@ def review_recap_compute_analysis_view(self, request, review_session_id): if cached_result is not None: return JsonResponse(cached_result) + if check_only: + return JsonResponse({"status": "empty"}) + # Use cache.add as a lock to prevent duplicate task dispatch. # Short TTL so lock auto-expires if the worker is killed before cleanup. computing_key = f"{combined_cache_key}:computing" diff --git a/backend/reviews/templates/reviews-recap.html b/backend/reviews/templates/reviews-recap.html index 8c8e2bbe24..595c499f36 100644 --- a/backend/reviews/templates/reviews-recap.html +++ b/backend/reviews/templates/reviews-recap.html @@ -682,6 +682,22 @@

🔗 Similar Talks

btn.addEventListener('click', function() { fetchAnalysis(false); }); recomputeBtn.addEventListener('click', function() { fetchAnalysis(true); }); + + // Auto-load cached results on page load (check only, don't trigger computation) + fetch(computeUrl + '?check=1', { + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function(response) { + if (!response.ok) return; + return response.json(); + }) + .then(function(data) { + if (!data || data.status === 'processing' || data.status === 'error' || data.status === 'empty') return; + handleResult(data); + }) + .catch(function() { + // Silently ignore errors on auto-load + }); })(); {% endblock %} diff --git a/backend/reviews/tests/test_recap.py b/backend/reviews/tests/test_recap.py index e2176430dd..366209378b 100644 --- a/backend/reviews/tests/test_recap.py +++ b/backend/reviews/tests/test_recap.py @@ -174,10 +174,7 @@ def test_recap_view_redirects_when_shortlist_not_visible(rf, mocker): response = admin.review_recap_view(request, review_session.id) assert response.status_code == 302 - assert ( - response.url - == f"/admin/reviews/reviewsession/{review_session.id}/change/" - ) + assert response.url == f"/admin/reviews/reviewsession/{review_session.id}/change/" # --- review_recap_compute_analysis_view tests --- @@ -194,9 +191,7 @@ def cache_get_side_effect(key): mock_cache_get = mocker.patch( "django.core.cache.cache.get", side_effect=cache_get_side_effect ) - mock_cache_add = mocker.patch( - "django.core.cache.cache.add", return_value=True - ) + mock_cache_add = mocker.patch("django.core.cache.cache.add", return_value=True) mocker.patch("django.core.cache.cache.set") mocker.patch("django.core.cache.cache.delete") mock_task = mocker.patch("reviews.tasks.compute_recap_analysis.apply_async") @@ -573,6 +568,56 @@ def cache_get_side_effect(key): mock_check.assert_not_called() +def test_compute_analysis_view_check_only_returns_empty_on_cache_miss(rf, mocker): + user, conference, review_session, submissions = _create_recap_setup() + + _, _, mock_task, mock_check = _mock_analysis_deps(mocker, cache_return=None) + + request = rf.get("/?check=1") + request.user = user + + admin = ReviewSessionAdmin(ReviewSession, AdminSite()) + response = admin.review_recap_compute_analysis_view(request, review_session.id) + + data = json.loads(response.content) + assert data == {"status": "empty"} + + # Task should NOT be dispatched in check-only mode + mock_task.assert_not_called() + mock_check.assert_not_called() + + +def test_compute_analysis_view_check_only_returns_cached_result(rf, mocker): + user, conference, review_session, submissions = _create_recap_setup() + sub1, sub2 = submissions + + cached_data = { + "submissions_list": [ + { + "id": sub1.id, + "title": str(sub1.title), + "type": sub1.type.name, + "speaker": sub1.speaker.display_name, + "similar": [], + }, + ], + "topic_clusters": {"topics": [], "outliers": [], "submission_topics": {}}, + } + + _, _, mock_task, _ = _mock_analysis_deps(mocker, cache_return=cached_data) + + request = rf.get("/?check=1") + request.user = user + + admin = ReviewSessionAdmin(ReviewSession, AdminSite()) + response = admin.review_recap_compute_analysis_view(request, review_session.id) + + data = json.loads(response.content) + assert data["submissions_list"][0]["id"] == sub1.id + + mock_task.assert_not_called() + + def test_error_cache_ttl_is_shorter_than_result_ttl(): from reviews.tasks import ERROR_CACHE_TTL, RESULT_CACHE_TTL