From 859bb089ca3ffa38e9c983b3b51051b643b8e96e Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Mon, 23 Feb 2026 16:50:33 -0600 Subject: [PATCH 1/3] save post, and piuuuurge --- apps/downloads/admin.py | 22 +++++++++++++++++++++- apps/downloads/models.py | 27 ++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/downloads/admin.py b/apps/downloads/admin.py index 397631490..4ea31747f 100644 --- a/apps/downloads/admin.py +++ b/apps/downloads/admin.py @@ -3,7 +3,15 @@ from django.contrib import admin from apps.cms.admin import ContentManageableModelAdmin, ContentManageableStackedInline -from apps.downloads.models import OS, Release, ReleaseFile +from apps.downloads.models import ( + OS, + Release, + ReleaseFile, + update_download_landing_sources_box, + update_homepage_download_box, + update_supernav, +) +from fastly.utils import purge_url @admin.register(OS) @@ -34,6 +42,18 @@ class ReleaseAdmin(ContentManageableModelAdmin): search_fields = ["name", "slug"] ordering = ["-release_date"] + def save_related(self, request, form, formsets, change): + """Update supernav after inline ReleaseFiles are saved and purge CDN.""" + super().save_related(request, form, formsets, change) + instance = form.instance + if instance.is_published: + update_supernav() + update_download_landing_sources_box() + update_homepage_download_box() + purge_url("/box/supernav-python-downloads/") + purge_url("/box/homepage-downloads/") + purge_url("/box/download-sources/") + def formfield_for_dbfield(self, db_field, request, **kwargs): """Add placeholder text to the release name field.""" field = super().formfield_for_dbfield(db_field, request, **kwargs) diff --git a/apps/downloads/models.py b/apps/downloads/models.py index 4b215cebf..a8feb85f8 100644 --- a/apps/downloads/models.py +++ b/apps/downloads/models.py @@ -5,7 +5,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.db import models -from django.db.models.signals import post_save +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.template.loader import render_to_string from django.urls import reverse @@ -332,6 +332,31 @@ def update_download_supernav_and_boxes(sender, instance, **kwargs): update_homepage_download_box() +def _update_boxes_for_release_file(instance): + """Update supernav and download boxes if the file's release is published.""" + if instance.release_id and instance.release.is_published: + update_supernav() + update_download_landing_sources_box() + update_homepage_download_box() + purge_url("/box/supernav-python-downloads/") + purge_url("/box/homepage-downloads/") + purge_url("/box/download-sources/") + + +@receiver(post_save, sender="downloads.ReleaseFile") +def update_boxes_on_release_file_save(sender, instance, **kwargs): + """Refresh supernav when a release file is added or changed.""" + if kwargs.get("raw", False): + return + _update_boxes_for_release_file(instance) + + +@receiver(post_delete, sender="downloads.ReleaseFile") +def update_boxes_on_release_file_delete(sender, instance, **kwargs): + """Refresh supernav when a release file is deleted.""" + _update_boxes_for_release_file(instance) + + class ReleaseFile(ContentManageable, NameSlugModel): """Individual files in a release. From 53bca3f1a0f3c7a1dd520c83984f8956e49efbeb Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Mon, 23 Feb 2026 16:51:01 -0600 Subject: [PATCH 2/3] be prettier --- apps/downloads/admin.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/apps/downloads/admin.py b/apps/downloads/admin.py index 4ea31747f..397631490 100644 --- a/apps/downloads/admin.py +++ b/apps/downloads/admin.py @@ -3,15 +3,7 @@ from django.contrib import admin from apps.cms.admin import ContentManageableModelAdmin, ContentManageableStackedInline -from apps.downloads.models import ( - OS, - Release, - ReleaseFile, - update_download_landing_sources_box, - update_homepage_download_box, - update_supernav, -) -from fastly.utils import purge_url +from apps.downloads.models import OS, Release, ReleaseFile @admin.register(OS) @@ -42,18 +34,6 @@ class ReleaseAdmin(ContentManageableModelAdmin): search_fields = ["name", "slug"] ordering = ["-release_date"] - def save_related(self, request, form, formsets, change): - """Update supernav after inline ReleaseFiles are saved and purge CDN.""" - super().save_related(request, form, formsets, change) - instance = form.instance - if instance.is_published: - update_supernav() - update_download_landing_sources_box() - update_homepage_download_box() - purge_url("/box/supernav-python-downloads/") - purge_url("/box/homepage-downloads/") - purge_url("/box/download-sources/") - def formfield_for_dbfield(self, db_field, request, **kwargs): """Add placeholder text to the release name field.""" field = super().formfield_for_dbfield(db_field, request, **kwargs) From d7cf0ff60e8471bcd003bb62170d1b36898c9231 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Mon, 23 Feb 2026 16:51:08 -0600 Subject: [PATCH 3/3] test: add tests for ReleaseFile signal-triggered box updates Co-Authored-By: Claude Opus 4.6 --- apps/downloads/tests/test_models.py | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/apps/downloads/tests/test_models.py b/apps/downloads/tests/test_models.py index 3a45e1d39..a409b51f0 100644 --- a/apps/downloads/tests/test_models.py +++ b/apps/downloads/tests/test_models.py @@ -1,4 +1,5 @@ import datetime as dt +from unittest.mock import patch from apps.downloads.models import Release, ReleaseFile from apps.downloads.tests.base import BaseDownloadTests @@ -232,3 +233,59 @@ def test_update_supernav_skips_os_without_files(self): # Android (no files) should not be present self.assertNotIn("android", content.lower()) + + @patch("apps.downloads.models.update_supernav") + @patch("apps.downloads.models.update_download_landing_sources_box") + @patch("apps.downloads.models.update_homepage_download_box") + def test_release_file_save_triggers_box_updates(self, mock_home, mock_sources, mock_supernav): + """Saving a ReleaseFile on a published release should update boxes.""" + mock_supernav.reset_mock() + mock_sources.reset_mock() + mock_home.reset_mock() + + ReleaseFile.objects.create( + os=self.windows, + release=self.python_3, + name="Windows installer", + url="/ftp/python/3.10.19/python-3.10.19.exe", + download_button=True, + ) + + mock_supernav.assert_called() + mock_sources.assert_called() + mock_home.assert_called() + + @patch("apps.downloads.models.update_supernav") + @patch("apps.downloads.models.update_download_landing_sources_box") + @patch("apps.downloads.models.update_homepage_download_box") + def test_release_file_save_skips_unpublished_release(self, mock_home, mock_sources, mock_supernav): + """Saving a ReleaseFile on a draft release should not update boxes.""" + mock_supernav.reset_mock() + mock_sources.reset_mock() + mock_home.reset_mock() + + ReleaseFile.objects.create( + os=self.windows, + release=self.draft_release, + name="Windows installer draft", + url="/ftp/python/9.7.2/python-9.7.2.exe", + ) + + mock_supernav.assert_not_called() + mock_sources.assert_not_called() + mock_home.assert_not_called() + + @patch("apps.downloads.models.update_supernav") + @patch("apps.downloads.models.update_download_landing_sources_box") + @patch("apps.downloads.models.update_homepage_download_box") + def test_release_file_delete_triggers_box_updates(self, mock_home, mock_sources, mock_supernav): + """Deleting a ReleaseFile on a published release should update boxes.""" + mock_supernav.reset_mock() + mock_sources.reset_mock() + mock_home.reset_mock() + + self.release_275_windows_32bit.delete() + + mock_supernav.assert_called() + mock_sources.assert_called() + mock_home.assert_called()