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 @@

Looking for a specific release?

{{ 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 @@

Looking for a specific release?

{% 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, "")