Skip to content

Commit a3f5c4f

Browse files
hugovkJacobCoffee
andauthored
Only show pre-releases when a release exists (#2998)
Co-authored-by: Jacob Coffee <jacob@z7x.org>
1 parent 5f16888 commit a3f5c4f

2 files changed

Lines changed: 45 additions & 18 deletions

File tree

apps/downloads/templatetags/download_tags.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,28 +187,27 @@ def render_active_releases():
187187

188188
found_eol = False
189189
for release in sorted_releases:
190+
minor = int(release.split(".")[1])
190191
info = release_cycle[release]
191192
status = info.get("status", "")
192193
first_release = info.get("first_release", "")
193194

194-
if status == "feature" and first_release:
195-
first_release = f"{first_release} (planned)"
196-
197-
if status == "feature":
195+
if status in ("planned", "feature", "prerelease"):
196+
# Only show pre-release entries once at least one alpha/beta/rc
197+
# has actually shipped (i.e. a published Release exists in the DB).
198+
if not Release.objects.latest_python3(minor):
199+
continue
200+
if first_release:
201+
first_release = f"{first_release} (planned)"
198202
status = "pre-release"
199203

200-
# Skip releases not yet in development
201-
if status in ("planned", "prerelease"):
202-
continue
203-
204204
if status == "end-of-life":
205205
# Include only the most recent EOL release
206206
if found_eol:
207207
continue
208208
found_eol = True
209209

210210
# Get last release for EOL versions
211-
minor = int(release.split(".")[1])
212211
last_release = Release.objects.latest_python3(minor)
213212
if last_release:
214213
status = format_html(

apps/downloads/tests/test_template_tags.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.test import TestCase, override_settings
66
from django.urls import reverse
77

8+
from apps.downloads.models import Release
89
from apps.downloads.templatetags.download_tags import get_eol_info, get_release_cycle_data, render_active_releases
910
from apps.downloads.tests.base import BaseDownloadTests
1011

@@ -14,8 +15,8 @@
1415
"3.9": {"status": "end-of-life", "end_of_life": "2025-10-31", "pep": 596},
1516
"3.10": {"status": "security", "end_of_life": "2026-10-04", "pep": 619},
1617
"3.14": {"status": "bugfix", "first_release": "2025-10-07", "end_of_life": "2030-10", "pep": 745},
17-
"3.15": {"status": "feature", "first_release": "2026-10-01", "end_of_life": "2031-10", "pep": 790},
18-
"3.16": {"status": "prerelease", "first_release": "2027-10-06", "end_of_life": "2032-10", "pep": 826},
18+
"3.15": {"status": "prerelease", "first_release": "2026-10-01", "end_of_life": "2031-10", "pep": 790},
19+
"3.16": {"status": "feature", "first_release": "2027-10-06", "end_of_life": "2032-10", "pep": 826},
1920
"3.17": {"status": "planned", "first_release": "2028-10-05", "end_of_life": "2033-10"},
2021
}
2122

@@ -177,6 +178,14 @@ class RenderActiveReleasesTests(BaseDownloadTests):
177178
def setUp(self):
178179
super().setUp()
179180
cache.clear()
181+
# A 3.15 prerelease exists in the DB (e.g. b1 shipped); 3.16 has no
182+
# published release yet, so it should remain hidden.
183+
self.python_3_15_b1 = Release.objects.create(
184+
version=Release.PYTHON3,
185+
name="Python 3.15.0b1",
186+
is_published=True,
187+
pre_release=True,
188+
)
180189

181190
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
182191
def test_versions_sorted_descending(self, mock_get_data):
@@ -186,12 +195,13 @@ def test_versions_sorted_descending(self, mock_get_data):
186195
result = render_active_releases()
187196

188197
versions = [r["version"] for r in result["releases"]]
189-
# 3.15, 3.14, 3.10, 3.9 (first EOL); 3.8 and 2.7 skipped (older EOL)
198+
# 3.15, 3.14, 3.10, 3.9 (first EOL); 3.8 and 2.7 skipped (older EOL).
199+
# 3.16 ('feature') and 3.17 ('planned') skipped: no published Release in DB.
190200
self.assertEqual(versions, ["3.15", "3.14", "3.10", "3.9"])
191201

192202
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
193-
def test_feature_status_becomes_prerelease(self, mock_get_data):
194-
"""Test that 'feature' status is converted to 'pre-release'."""
203+
def test_prerelease_status_relabelled(self, mock_get_data):
204+
"""Test that 'prerelease' status is converted to 'pre-release' for display."""
195205
mock_get_data.return_value = MOCK_RELEASE_CYCLE
196206

197207
result = render_active_releases()
@@ -201,8 +211,8 @@ def test_feature_status_becomes_prerelease(self, mock_get_data):
201211
self.assertEqual(prerelease["status"], "pre-release")
202212

203213
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
204-
def test_feature_first_release_shows_planned(self, mock_get_data):
205-
"""Test that feature releases show (planned) in first_release."""
214+
def test_prerelease_first_release_shows_planned(self, mock_get_data):
215+
"""Test that pre-release entries show (planned) in first_release."""
206216
mock_get_data.return_value = MOCK_RELEASE_CYCLE
207217

208218
result = render_active_releases()
@@ -237,16 +247,34 @@ def test_eol_status_includes_last_release_link(self, mock_get_data):
237247
self.assertIn("<a href=", status)
238248

239249
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
240-
def test_planned_and_prerelease_releases_excluded(self, mock_get_data):
241-
"""Test that planned and prerelease releases are not shown."""
250+
def test_pre_release_versions_without_shipped_release_excluded(self, mock_get_data):
251+
"""Pre-release statuses ('feature', 'planned') without a shipped release stay hidden."""
242252
mock_get_data.return_value = MOCK_RELEASE_CYCLE
243253

244254
result = render_active_releases()
245255

246256
versions = [r["version"] for r in result["releases"]]
257+
# 3.16 ('feature') and 3.17 ('planned') have no published Release in the DB.
247258
self.assertNotIn("3.16", versions)
248259
self.assertNotIn("3.17", versions)
249260

261+
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
262+
def test_feature_version_shown_once_alpha_ships(self, mock_get_data):
263+
"""A 'feature' (in-development on main) version is shown once an alpha ships."""
264+
mock_get_data.return_value = MOCK_RELEASE_CYCLE
265+
Release.objects.create(
266+
version=Release.PYTHON3,
267+
name="Python 3.16.0a1",
268+
is_published=True,
269+
pre_release=True,
270+
)
271+
272+
result = render_active_releases()
273+
274+
entry = next(r for r in result["releases"] if r["version"] == "3.16")
275+
self.assertEqual(entry["status"], "pre-release")
276+
self.assertEqual(entry["first_release"], "2027-10-06 (planned)")
277+
250278
@mock.patch("apps.downloads.templatetags.download_tags.get_release_cycle_data")
251279
def test_api_failure_returns_empty_releases(self, mock_get_data):
252280
"""Test that API failure returns empty releases list."""

0 commit comments

Comments
 (0)