diff --git a/apps/downloads/models.py b/apps/downloads/models.py
index 8af0a1c1d..c3dc00d52 100644
--- a/apps/downloads/models.py
+++ b/apps/downloads/models.py
@@ -114,6 +114,29 @@ def get_absolute_url(self):
return self.release_page.get_absolute_url()
return reverse("download:download_release_detail", kwargs={"release_slug": self.slug})
+ @property
+ def corrected_release_notes_url(self):
+ """Return the release notes URL, converting dead hg.python.org links to GitHub.
+
+ Old Mercurial-hosted URLs (hg.python.org) are no longer reachable.
+ This property remaps them to their equivalent paths on GitHub so that
+ the "Release notes" links on the downloads page still work for legacy
+ releases (3.3.6 and earlier).
+
+ Example::
+
+ http://hg.python.org/cpython/file/v3.3.6/Misc/NEWS
+ → https://github.com/python/cpython/blob/v3.3.6/Misc/NEWS
+ """
+ url = self.release_notes_url
+ if not url:
+ return url
+ match = re.match(r"https?://hg\.python\.org/cpython/file/([^/]+)/(.+)", url)
+ if match:
+ tag, path = match.group(1), match.group(2)
+ return f"https://github.com/python/cpython/blob/{tag}/{path}"
+ return url
+
def download_file_for_os(self, os_slug):
"""Given an OS slug return the appropriate download file."""
try:
@@ -430,3 +453,4 @@ class Meta:
violation_error_message="All file URLs must begin with 'https://www.python.org/'",
),
]
+
diff --git a/apps/downloads/templates/downloads/index.html b/apps/downloads/templates/downloads/index.html
index d87b32823..1c04f5df0 100644
--- a/apps/downloads/templates/downloads/index.html
+++ b/apps/downloads/templates/downloads/index.html
@@ -76,7 +76,7 @@
{{ r.name }}
{{ r.release_date|date }}
Download
- Release notes
+ {% if r.corrected_release_notes_url %}Release notes{% else %}Release notes{% endif %}
{% endfor %}
@@ -127,3 +127,4 @@
{% box 'download-pgp' %}
{% endblock content %}
+
diff --git a/apps/downloads/tests/test_models.py b/apps/downloads/tests/test_models.py
index d1d4c97b3..1426cccd3 100644
--- a/apps/downloads/tests/test_models.py
+++ b/apps/downloads/tests/test_models.py
@@ -311,3 +311,53 @@ def test_release_file_urls_not_python_dot_org(self):
name="Windows installer draft",
**kwargs,
)
+
+class ReleaseNotesURLTests(BaseDownloadTests):
+ """Tests for Release.corrected_release_notes_url property."""
+
+ def test_hg_url_converted_to_github(self):
+ """An hg.python.org URL is remapped to its GitHub equivalent."""
+ release = Release.objects.create(
+ version=Release.PYTHON3,
+ name="Python 3.3.6",
+ is_published=True,
+ release_notes_url="http://hg.python.org/cpython/file/v3.3.6/Misc/NEWS",
+ )
+ self.assertEqual(
+ release.corrected_release_notes_url,
+ "https://github.com/python/cpython/blob/v3.3.6/Misc/NEWS",
+ )
+
+ def test_https_hg_url_also_converted(self):
+ """An https hg.python.org URL is also remapped to GitHub."""
+ release = Release.objects.create(
+ version=Release.PYTHON3,
+ name="Python 3.2.6",
+ is_published=True,
+ release_notes_url="https://hg.python.org/cpython/file/v3.2.6/Misc/NEWS",
+ )
+ self.assertEqual(
+ release.corrected_release_notes_url,
+ "https://github.com/python/cpython/blob/v3.2.6/Misc/NEWS",
+ )
+
+ def test_modern_url_returned_unchanged(self):
+ """A non-hg URL (e.g. already on GitHub) is returned unchanged."""
+ url = "https://github.com/python/cpython/blob/v3.12.0/Misc/NEWS.d"
+ release = Release.objects.create(
+ version=Release.PYTHON3,
+ name="Python 3.12.0",
+ is_published=True,
+ release_notes_url=url,
+ )
+ self.assertEqual(release.corrected_release_notes_url, url)
+
+ def test_empty_url_returns_empty(self):
+ """An empty release_notes_url returns an empty string."""
+ release = Release.objects.create(
+ version=Release.PYTHON3,
+ name="Python 3.11.0",
+ is_published=True,
+ release_notes_url="",
+ )
+ self.assertEqual(release.corrected_release_notes_url, "")