Skip to content

Commit 9e22f88

Browse files
authored
Merge branch 'main' into sha256-release-pydotorg
2 parents 58981c4 + e94fa01 commit 9e22f88

File tree

16 files changed

+390
-7
lines changed

16 files changed

+390
-7
lines changed

downloads/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ def formfield_for_dbfield(self, db_field, request, **kwargs):
3131
if db_field.name == "name":
3232
field.widget.attrs["placeholder"] = "Python 3.X.YaN"
3333
return field
34+
35+
class Media:
36+
js = ["js/admin/releaseAdmin.js"]

downloads/managers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def latest_python3(self, minor_version: int | None = None):
3535
pattern = rf"^Python 3\.{minor_version}\."
3636
return self.python3().filter(name__regex=pattern).order_by("-release_date")
3737

38+
def latest_prerelease(self):
39+
return self.python3().filter(pre_release=True).order_by("-release_date")
40+
3841
def latest_pymanager(self):
3942
return self.pymanager().filter(is_latest=True)
4043

@@ -52,5 +55,8 @@ def latest_python2(self):
5255
def latest_python3(self, minor_version: int | None = None):
5356
return self.get_queryset().latest_python3(minor_version).first()
5457

58+
def latest_prerelease(self):
59+
return self.get_queryset().latest_prerelease().first()
60+
5561
def latest_pymanager(self):
5662
return self.get_queryset().latest_pymanager().first()

downloads/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def purge_fastly_download_pages(sender, instance, **kwargs):
298298
match = re.match(r'^3\.(\d+)', version)
299299
if match:
300300
purge_url(f'/downloads/latest/python3.{match.group(1)}/')
301+
purge_url('/downloads/latest/prerelease/')
301302
purge_url('/downloads/latest/pymanager/')
302303
purge_url('/downloads/macos/')
303304
purge_url('/downloads/source/')

downloads/tests/test_models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@ def test_latest_python3(self):
7474
latest_3_99 = Release.objects.latest_python3(minor_version=99)
7575
self.assertIsNone(latest_3_99)
7676

77+
def test_latest_prerelease(self):
78+
latest_prerelease = Release.objects.latest_prerelease()
79+
self.assertEqual(latest_prerelease, self.pre_release)
80+
81+
# Create a newer prerelease with a future date
82+
newer_prerelease = Release.objects.create(
83+
version=Release.PYTHON3,
84+
name="Python 3.9.99",
85+
is_published=True,
86+
pre_release=True,
87+
release_date=self.pre_release.release_date + dt.timedelta(days=1),
88+
)
89+
latest_prerelease = Release.objects.latest_prerelease()
90+
self.assertEqual(latest_prerelease, newer_prerelease)
91+
self.assertNotEqual(latest_prerelease, self.pre_release)
92+
93+
def test_latest_prerelease_when_no_prerelease(self):
94+
# Delete the prerelease
95+
self.pre_release.delete()
96+
latest_prerelease = Release.objects.latest_prerelease()
97+
self.assertIsNone(latest_prerelease)
98+
7799
def test_get_version(self):
78100
self.assertEqual(self.release_275.name, 'Python 2.7.5')
79101
self.assertEqual(self.release_275.get_version(), '2.7.5')

downloads/tests/test_views.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,37 @@ def test_download_release_detail(self):
4646
response = self.client.get(url)
4747
self.assertEqual(response.status_code, 404)
4848

49+
def test_download_release_detail_not_superseded(self):
50+
"""Test that latest releases and Python 2 do not show a superseded notice."""
51+
for release in [self.python_3, self.python_3_8_20, self.release_275]:
52+
with self.subTest(release=release.name):
53+
url = reverse(
54+
"download:download_release_detail",
55+
kwargs={"release_slug": release.slug},
56+
)
57+
response = self.client.get(url)
58+
self.assertEqual(response.status_code, 200)
59+
self.assertNotIn("latest_in_series", response.context)
60+
self.assertNotContains(response, "has been superseded by")
61+
62+
def test_download_release_detail_superseded(self):
63+
"""Test that older releases show a superseded notice."""
64+
tests = [
65+
(self.python_3_10_18, self.python_3),
66+
(self.python_3_8_19, self.python_3_8_20),
67+
]
68+
for old_release, latest_release in tests:
69+
with self.subTest(release=old_release.name):
70+
url = reverse(
71+
"download:download_release_detail",
72+
kwargs={"release_slug": old_release.slug},
73+
)
74+
response = self.client.get(url)
75+
self.assertEqual(response.status_code, 200)
76+
self.assertEqual(response.context["latest_in_series"], latest_release)
77+
self.assertContains(response, "has been superseded by")
78+
self.assertContains(response, latest_release.name)
79+
4980
def test_download_os_list(self):
5081
url = reverse('download:download_os_list', kwargs={'slug': self.linux.slug})
5182
response = self.client.get(url)
@@ -95,6 +126,18 @@ def test_latest_python3x_redirects(self):
95126
response = self.client.get(url)
96127
self.assertRedirects(response, reverse("download:download"))
97128

129+
def test_latest_prerelease_redirect(self):
130+
url = reverse("download:download_latest_prerelease")
131+
response = self.client.get(url)
132+
self.assertRedirects(response, self.pre_release.get_absolute_url())
133+
134+
def test_latest_prerelease_redirect_when_no_prerelease(self):
135+
# Delete the prerelease to test fallback
136+
self.pre_release.delete()
137+
url = reverse("download:download_latest_prerelease")
138+
response = self.client.get(url)
139+
self.assertRedirects(response, reverse("download:download"))
140+
98141
def test_redirect_page_object_to_release_detail_page(self):
99142
self.release_275.release_page = None
100143
self.release_275.save()

downloads/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
re_path(r'latest/python2/?$', views.DownloadLatestPython2.as_view(), name='download_latest_python2'),
77
re_path(r'latest/python3/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3'),
88
re_path(r'latest/python3\.(?P<minor>\d+)/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3x'),
9+
re_path(r'latest/prerelease/?$', views.DownloadLatestPrerelease.as_view(), name='download_latest_prerelease'),
910
re_path(r'latest/pymanager/?$', views.DownloadLatestPyManager.as_view(), name='download_latest_pymanager'),
1011
re_path(r'latest/?$', views.DownloadLatestPython3.as_view(), name='download_latest_python3'),
1112
path('operating-systems/', views.DownloadFullOSList.as_view(), name='download_full_os_list'),

downloads/views.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any
22

3+
import re
34
from datetime import datetime
45

56
from django.db.models import Case, IntegerField, Prefetch, When
@@ -47,6 +48,23 @@ def get_redirect_url(self, **kwargs):
4748
return reverse("downloads:download")
4849

4950

51+
class DownloadLatestPrerelease(RedirectView):
52+
"""Redirect to latest Python 3 prerelease"""
53+
54+
permanent = False
55+
56+
def get_redirect_url(self, **kwargs):
57+
try:
58+
latest_prerelease = Release.objects.latest_prerelease()
59+
except Release.DoesNotExist:
60+
latest_prerelease = None
61+
62+
if latest_prerelease:
63+
return latest_prerelease.get_absolute_url()
64+
else:
65+
return reverse("downloads:download")
66+
67+
5068
class DownloadLatestPyManager(RedirectView):
5169
""" Redirect to latest Python install manager release """
5270
permanent = False
@@ -199,6 +217,17 @@ def get_context_data(self, **kwargs):
199217
)
200218
)
201219

220+
# Find the latest release in the feature series (such as 3.14.x)
221+
# to show a "superseded by" notice on older releases
222+
version = self.object.get_version()
223+
if version and self.object.version == Release.PYTHON3:
224+
match = re.match(r"^3\.(\d+)", version)
225+
if match:
226+
minor_version = int(match.group(1))
227+
latest_in_series = Release.objects.latest_python3(minor_version)
228+
if latest_in_series and latest_in_series.pk != self.object.pk:
229+
context["latest_in_series"] = latest_in_series
230+
202231
return context
203232

204233

0 commit comments

Comments
 (0)