diff --git a/downloads/managers.py b/downloads/managers.py index 22da0cdd0..f692524ce 100644 --- a/downloads/managers.py +++ b/downloads/managers.py @@ -35,6 +35,9 @@ def latest_python3(self, minor_version: int | None = None): pattern = rf"^Python 3\.{minor_version}\." return self.python3().filter(name__regex=pattern).order_by("-release_date") + def latest_prerelease(self): + return self.python3().filter(pre_release=True).order_by("-release_date") + def latest_pymanager(self): return self.pymanager().filter(is_latest=True) @@ -52,5 +55,8 @@ def latest_python2(self): def latest_python3(self, minor_version: int | None = None): return self.get_queryset().latest_python3(minor_version).first() + def latest_prerelease(self): + return self.get_queryset().latest_prerelease().first() + def latest_pymanager(self): return self.get_queryset().latest_pymanager().first() diff --git a/downloads/models.py b/downloads/models.py index f37a041d0..fb651c29a 100644 --- a/downloads/models.py +++ b/downloads/models.py @@ -298,6 +298,7 @@ def purge_fastly_download_pages(sender, instance, **kwargs): match = re.match(r'^3\.(\d+)', version) if match: purge_url(f'/downloads/latest/python3.{match.group(1)}/') + purge_url('/downloads/latest/prerelease/') purge_url('/downloads/latest/pymanager/') purge_url('/downloads/macos/') purge_url('/downloads/source/') diff --git a/downloads/tests/test_models.py b/downloads/tests/test_models.py index 1c8e9ba47..1f260e08a 100644 --- a/downloads/tests/test_models.py +++ b/downloads/tests/test_models.py @@ -74,6 +74,28 @@ def test_latest_python3(self): latest_3_99 = Release.objects.latest_python3(minor_version=99) self.assertIsNone(latest_3_99) + def test_latest_prerelease(self): + latest_prerelease = Release.objects.latest_prerelease() + self.assertEqual(latest_prerelease, self.pre_release) + + # Create a newer prerelease with a future date + newer_prerelease = Release.objects.create( + version=Release.PYTHON3, + name="Python 3.9.99", + is_published=True, + pre_release=True, + release_date=self.pre_release.release_date + dt.timedelta(days=1), + ) + latest_prerelease = Release.objects.latest_prerelease() + self.assertEqual(latest_prerelease, newer_prerelease) + self.assertNotEqual(latest_prerelease, self.pre_release) + + def test_latest_prerelease_when_no_prerelease(self): + # Delete the prerelease + self.pre_release.delete() + latest_prerelease = Release.objects.latest_prerelease() + self.assertIsNone(latest_prerelease) + def test_get_version(self): self.assertEqual(self.release_275.name, 'Python 2.7.5') self.assertEqual(self.release_275.get_version(), '2.7.5') diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index 1fbb687d0..5c5471d5a 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -95,6 +95,18 @@ def test_latest_python3x_redirects(self): response = self.client.get(url) self.assertRedirects(response, reverse("download:download")) + def test_latest_prerelease_redirect(self): + url = reverse("download:download_latest_prerelease") + response = self.client.get(url) + self.assertRedirects(response, self.pre_release.get_absolute_url()) + + def test_latest_prerelease_redirect_when_no_prerelease(self): + # Delete the prerelease to test fallback + self.pre_release.delete() + url = reverse("download:download_latest_prerelease") + response = self.client.get(url) + self.assertRedirects(response, reverse("download:download")) + def test_redirect_page_object_to_release_detail_page(self): self.release_275.release_page = None self.release_275.save() diff --git a/downloads/urls.py b/downloads/urls.py index 75dcef211..01f055fde 100644 --- a/downloads/urls.py +++ b/downloads/urls.py @@ -6,6 +6,7 @@ re_path(r'latest/python2/?$', views.DownloadLatestPython2.as_view(), name='download_latest_python2'), re_path(r'latest/python3/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3'), re_path(r'latest/python3\.(?P\d+)/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3x'), + re_path(r'latest/prerelease/?$', views.DownloadLatestPrerelease.as_view(), name='download_latest_prerelease'), re_path(r'latest/pymanager/?$', views.DownloadLatestPyManager.as_view(), name='download_latest_pymanager'), re_path(r'latest/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3'), path('operating-systems/', views.DownloadFullOSList.as_view(), name='download_full_os_list'), diff --git a/downloads/views.py b/downloads/views.py index 1d69a234c..fd1141c14 100644 --- a/downloads/views.py +++ b/downloads/views.py @@ -47,6 +47,23 @@ def get_redirect_url(self, **kwargs): return reverse("downloads:download") +class DownloadLatestPrerelease(RedirectView): + """Redirect to latest Python 3 prerelease""" + + permanent = False + + def get_redirect_url(self, **kwargs): + try: + latest_prerelease = Release.objects.latest_prerelease() + except Release.DoesNotExist: + latest_prerelease = None + + if latest_prerelease: + return latest_prerelease.get_absolute_url() + else: + return reverse("downloads:download") + + class DownloadLatestPyManager(RedirectView): """ Redirect to latest Python install manager release """ permanent = False diff --git a/fixtures/boxes.json b/fixtures/boxes.json index df66827b5..17103ee2f 100644 --- a/fixtures/boxes.json +++ b/fixtures/boxes.json @@ -642,9 +642,9 @@ "created": "2014-02-13T21:06:57.376Z", "updated": "2021-07-29T21:39:50.973Z", "label": "download-banner", - "content": "

\r\n Looking for Python with a different OS? Python for\r\n Windows,\r\n Linux/UNIX,\r\n macOS,\r\n Other\r\n

\r\n\r\n

\r\n Want to help test development versions of Python?\r\n Prereleases,\r\n Docker images \r\n

\r\n\r\n

\r\n Looking for Python 2.7? See below for specific releases\r\n

", + "content": "

\r\n Looking for Python with a different OS? Python for\r\n Windows,\r\n Linux/Unix,\r\n macOS,\r\n Android,\r\n other\r\n

\r\n

\r\n Want to help test development versions of Python 3.15?\r\n Pre-releases,\r\n Docker images \r\n

", "content_markup_type": "html", - "_content_rendered": "

\r\n Looking for Python with a different OS? Python for\r\n Windows,\r\n Linux/UNIX,\r\n macOS,\r\n Other\r\n

\r\n\r\n

\r\n Want to help test development versions of Python?\r\n Prereleases,\r\n Docker images \r\n

\r\n\r\n

\r\n Looking for Python 2.7? See below for specific releases\r\n

" + "_content_rendered": "

\r\n Looking for Python with a different OS? Python for\r\n Windows,\r\n Linux/Unix,\r\n macOS,\r\n Android,\r\n other\r\n

\r\n

\r\n Want to help test development versions of Python 3.15?\r\n Pre-releases,\r\n Docker images \r\n

" } }, { @@ -702,9 +702,9 @@ "created": "2020-10-05T17:33:33.157Z", "updated": "2022-05-17T17:22:45.210Z", "label": "downloads-active-releases", - "content": "
\r\n Python version\r\n Maintenance status\r\n First released\r\n End of support\r\n Release schedule\r\n
\r\n
    \r\n
  1. \r\n 3.10\r\n bugfix\r\n 2021-10-04\r\n 2026-10\r\n PEP 619\r\n
  2. \r\n
  3. \r\n 3.9\r\n security\r\n 2020-10-05\r\n 2025-10\r\n PEP 596\r\n
  4. \r\n
  5. \r\n 3.8\r\n security\r\n 2019-10-14\r\n 2024-10\r\n PEP 569\r\n
  6. \r\n
  7. \r\n 3.7\r\n security\r\n 2018-06-27\r\n 2023-06-27\r\n PEP 537\r\n
  8. \r\n
  9. \r\n 2.7\r\n end-of-life\r\n 2010-07-03\r\n 2020-01-01\r\n PEP 373\r\n
  10. \r\n
", + "content": "
\r\n Python version\r\n Maintenance status\r\n  \r\n First released\r\n End of support\r\n Release schedule\r\n
\r\n
    \r\n
  1. \r\n 3.15\r\n pre-release\r\n Download\r\n 2026-10-07 (planned)\r\n 2031-10\r\n PEP 790\r\n
  2. \r\n
  3. \r\n 3.14\r\n bugfix\r\n Download\r\n 2025-10-07\r\n 2030-10\r\n PEP 745\r\n
  4. \r\n
  5. \r\n 3.13\r\n bugfix\r\n Download\r\n 2024-10-07\r\n 2029-10\r\n PEP 719\r\n
  6. \r\n
  7. \r\n 3.12\r\n security\r\n Download\r\n 2023-10-02\r\n 2028-10\r\n PEP 693\r\n
  8. \r\n
  9. \r\n 3.11\r\n security\r\n Download\r\n 2022-10-24\r\n 2027-10\r\n PEP 664\r\n
  10. \r\n
  11. \r\n 3.10\r\n security\r\n Download\r\n 2021-10-04\r\n 2026-10\r\n PEP 619\r\n
  12. \r\n
  13. \r\n 3.9\r\n end of life, last release was 3.9.25\r\n Download\r\n 2020-10-05\r\n 2025-10-31\r\n PEP 596\r\n
  14. \r\n
", "content_markup_type": "html", - "_content_rendered": "
\r\n Python version\r\n Maintenance status\r\n First released\r\n End of support\r\n Release schedule\r\n
\r\n
    \r\n
  1. \r\n 3.10\r\n bugfix\r\n 2021-10-04\r\n 2026-10\r\n PEP 619\r\n
  2. \r\n
  3. \r\n 3.9\r\n security\r\n 2020-10-05\r\n 2025-10\r\n PEP 596\r\n
  4. \r\n
  5. \r\n 3.8\r\n security\r\n 2019-10-14\r\n 2024-10\r\n PEP 569\r\n
  6. \r\n
  7. \r\n 3.7\r\n security\r\n 2018-06-27\r\n 2023-06-27\r\n PEP 537\r\n
  8. \r\n
  9. \r\n 2.7\r\n end-of-life\r\n 2010-07-03\r\n 2020-01-01\r\n PEP 373\r\n
  10. \r\n
" + "_content_rendered": "
\r\n Python version\r\n Maintenance status\r\n  \r\n First released\r\n End of support\r\n Release schedule\r\n
\r\n
    \r\n
  1. \r\n 3.15\r\n pre-release\r\n Download\r\n 2026-10-07 (planned)\r\n 2031-10\r\n PEP 790\r\n
  2. \r\n
  3. \r\n 3.14\r\n bugfix\r\n Download\r\n 2025-10-07\r\n 2030-10\r\n PEP 745\r\n
  4. \r\n
  5. \r\n 3.13\r\n bugfix\r\n Download\r\n 2024-10-07\r\n 2029-10\r\n PEP 719\r\n
  6. \r\n
  7. \r\n 3.12\r\n security\r\n Download\r\n 2023-10-02\r\n 2028-10\r\n PEP 693\r\n
  8. \r\n
  9. \r\n 3.11\r\n security\r\n Download\r\n 2022-10-24\r\n 2027-10\r\n PEP 664\r\n
  10. \r\n
  11. \r\n 3.10\r\n security\r\n Download\r\n 2021-10-04\r\n 2026-10\r\n PEP 619\r\n
  12. \r\n
  13. \r\n 3.9\r\n end of life, last release was 3.9.25\r\n Download\r\n 2020-10-05\r\n 2025-10-31\r\n PEP 596\r\n
  14. \r\n
" } }, {