From 85b20734b85fd2f6f186c47fbac3f2a328aadc56 Mon Sep 17 00:00:00 2001 From: skaphan Date: Wed, 4 Mar 2026 12:42:20 -0500 Subject: [PATCH 1/4] Return friendly page instead of 404 for missing embed slugs When an embedded visualization URL references a slug that no longer exists, return a user-friendly HTML page (200) instead of Django default 404. This displays cleanly inside iframes and avoids cross-origin error handling issues. Cache headers prevent the response from being cached so the real visualization appears once re-created. Co-Authored-By: Claude Opus 4.6 --- visualizer/tests/testSimple.py | 9 +++++++++ visualizer/views.py | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/visualizer/tests/testSimple.py b/visualizer/tests/testSimple.py index f5df1319..6b12f9af 100644 --- a/visualizer/tests/testSimple.py +++ b/visualizer/tests/testSimple.py @@ -293,6 +293,15 @@ def test_embedly_translation(self): response = self.client.get(visualizeUrl) self.assertRedirects(response, expectedBaseURL + 'barchart-interactive', status_code=301) + def test_embedded_404_returns_friendly_page(self): + """ + When an embedded visualization slug doesn't exist, return a friendly + HTML page (200) instead of a 404, so it displays nicely in iframes. + """ + response = self.client.get('/ve/nonexistent-slug') + self.assertEqual(response.status_code, 200) + self.assertIn(b'Visualization Not Found', response.content) + @patch('visualizer.wikipedia.wikipedia.WikipediaExport._get_todays_date_string') def test_wikicode(self, mockGetDateString): """ Validate that the wikicode can be generated and hasn't inadvertently changed """ diff --git a/visualizer/views.py b/visualizer/views.py index d1b51011..c63d993d 100644 --- a/visualizer/views.py +++ b/visualizer/views.py @@ -12,12 +12,13 @@ from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.core.cache import cache -from django.http import JsonResponse, HttpResponse +from django.http import Http404, JsonResponse, HttpResponse from django.shortcuts import render from django.templatetags.static import static from django.urls import Resolver404 from django.urls import resolve from django.urls import reverse +from django.utils.cache import patch_cache_control from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.clickjacking import xframe_options_exempt @@ -207,6 +208,23 @@ class VisualizeEmbedded(DetailView): model = JsonConfig template_name = 'visualizer/visualize-embedded.html' + def get(self, request, *args, **kwargs): + try: + return super().get(request, *args, **kwargs) + except Http404: + response = HttpResponse( + '' + '' + '

Visualization Not Found

' + '

This visualization is no longer available.
' + 'Please re-send the election data to generate a new visualization.

' + '', + content_type='text/html', + ) + patch_cache_control(response, no_store=True, no_cache=True, max_age=0) + return response + def get_context_data(self, **kwargs): config = super().get_context_data(**kwargs) From 0fdb62494239a1eac21c398c009c7180b44792cc Mon Sep 17 00:00:00 2001 From: Armin Samii Date: Wed, 4 Mar 2026 16:22:39 -0500 Subject: [PATCH 2/4] Return 404 status for unavailable visualizations --- visualizer/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/visualizer/views.py b/visualizer/views.py index c63d993d..2059b8fd 100644 --- a/visualizer/views.py +++ b/visualizer/views.py @@ -221,6 +221,7 @@ def get(self, request, *args, **kwargs): 'Please re-send the election data to generate a new visualization.

' '', content_type='text/html', + status=404, ) patch_cache_control(response, no_store=True, no_cache=True, max_age=0) return response From 4fdf2ffbb15cc789d67f57d6535bb70257299976 Mon Sep 17 00:00:00 2001 From: skaphan Date: Thu, 5 Mar 2026 06:13:26 -0500 Subject: [PATCH 3/4] Add reset-db.sh for easy database reset after branch switching Co-Authored-By: Claude Opus 4.6 --- scripts/reset-db.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 scripts/reset-db.sh diff --git a/scripts/reset-db.sh b/scripts/reset-db.sh new file mode 100755 index 00000000..24f378fa --- /dev/null +++ b/scripts/reset-db.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Reset the SQLite database and re-run all migrations. +# Use after switching branches with incompatible migration history. +set -e + +source venv/bin/activate +source .env + +rm -f db.sqlite3 +python manage.py migrate + +# Create API-enabled admin user (matches docker-entrypoint.sh) +python manage.py shell -c " +from django.contrib.auth import get_user_model +User = get_user_model() +if not User.objects.filter(username='skaphan').exists(): + user = User.objects.create_superuser('skaphan', 'sjk@kaphan.org', 'rcvisacc0unt') + user.userprofile.canUseApi = True + user.userprofile.save() + print('Created API user skaphan with API access') +else: + print('API user skaphan already exists') +" + +echo "Database reset complete." From 28c7eb674d925557f610db79da018e0ddc803fa3 Mon Sep 17 00:00:00 2001 From: skaphan Date: Thu, 5 Mar 2026 18:51:48 -0500 Subject: [PATCH 4/4] Update friendly-embed-404 test to expect 404 status The view now correctly returns 404 with friendly HTML content. Update the test to match. Co-Authored-By: Claude Opus 4.6 --- visualizer/tests/testSimple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/visualizer/tests/testSimple.py b/visualizer/tests/testSimple.py index 6b12f9af..139cb4d4 100644 --- a/visualizer/tests/testSimple.py +++ b/visualizer/tests/testSimple.py @@ -296,10 +296,10 @@ def test_embedly_translation(self): def test_embedded_404_returns_friendly_page(self): """ When an embedded visualization slug doesn't exist, return a friendly - HTML page (200) instead of a 404, so it displays nicely in iframes. + HTML page with a 404 status, so it displays nicely in iframes. """ response = self.client.get('/ve/nonexistent-slug') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 404) self.assertIn(b'Visualization Not Found', response.content) @patch('visualizer.wikipedia.wikipedia.WikipediaExport._get_todays_date_string')