Skip to content

Commit e4883ee

Browse files
authored
feat(#1612): add rss feed for latest downloads (#2569)
* feat: add rss feed for latest downloads * Update downloads/views.py * fix: query DB for releas * fix: query DB for releas * chore: add missing types * fix: update naive datetime, remove staticmethod * fix: remove staticmethod chore: apply formatting and ruuuuuff * chore: no logging needed after working * tests: add them * chore: address code reviews * chore: remove unused imports * chore: remove unused code * chore: remove unused code * fix: purge cdn cache * revert: put the code back, john
1 parent 306a73d commit e4883ee

File tree

4 files changed

+93
-0
lines changed

4 files changed

+93
-0
lines changed

downloads/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def purge_fastly_download_pages(sender, instance, **kwargs):
272272
if instance.is_published:
273273
# Purge our common pages
274274
purge_url('/downloads/')
275+
purge_url('/downloads/feed.rss')
275276
purge_url('/downloads/latest/python2/')
276277
purge_url('/downloads/latest/python3/')
277278
purge_url('/downloads/macos/')

downloads/tests/test_views.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,3 +554,45 @@ def test_filter_release_file_delete_by_release(self):
554554
headers={"authorization": self.Authorization}
555555
)
556556
self.assertEqual(response.status_code, 405)
557+
558+
class ReleaseFeedTests(BaseDownloadTests):
559+
"""Tests for the downloads/feed.rss endpoint.
560+
561+
Content is ensured via setUp in BaseDownloadTests.
562+
"""
563+
564+
url = reverse("downloads:feed")
565+
566+
567+
def test_endpoint_reachable(self) -> None:
568+
response = self.client.get(self.url)
569+
self.assertEqual(response.status_code, 200)
570+
571+
def test_feed_content(self) -> None:
572+
"""Ensure feed content is as expected.
573+
574+
Some things we want to check:
575+
- Feed title, description, pubdate
576+
- Feed items (releases) are in the correct order
577+
- We get the expected number of releases (10)
578+
"""
579+
response = self.client.get(self.url)
580+
content = response.content.decode()
581+
582+
self.assertIn("Python 2.7.5", content)
583+
self.assertIn("Python 3.10", content)
584+
# Published but hidden show up in the API and thus the feed
585+
self.assertIn("Python 0.0.0", content)
586+
587+
# No unpublished releases
588+
self.assertNotIn("Python 9.7.2", content)
589+
590+
# Pre-releases are shown
591+
self.assertIn("Python 3.9.90", content)
592+
593+
def test_feed_item_count(self) -> None:
594+
response = self.client.get(self.url)
595+
content = response.content.decode()
596+
597+
# In BaseDownloadTests, we create 5 releases, 4 of which are published, 1 of those published are hidden..
598+
self.assertEqual(content.count("<item>"), 4)

downloads/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
path('release/<slug:release_slug>/', views.DownloadReleaseDetail.as_view(), name='download_release_detail'),
1010
path('<slug:slug>/', views.DownloadOSList.as_view(), name='download_os_list'),
1111
path('', views.DownloadHome.as_view(), name='download'),
12+
path("feed.rss", views.ReleaseFeed(), name="feed"),
1213
]

downloads/views.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from typing import Any
2+
3+
from datetime import datetime
4+
15
from django.db.models import Prefetch
26
from django.urls import reverse
7+
from django.utils import timezone
38
from django.views.generic import DetailView, TemplateView, ListView, RedirectView
49
from django.http import Http404
10+
from django.contrib.syndication.views import Feed
11+
from django.utils.feedgenerator import Rss201rev2Feed
512

613
from .models import OS, Release, ReleaseFile
714

@@ -147,3 +154,45 @@ def get_context_data(self, **kwargs):
147154
)
148155

149156
return context
157+
158+
159+
class ReleaseFeed(Feed):
160+
"""Generate an RSS feed of the latest Python releases.
161+
162+
.. note:: It may seem like these are unused methods, but the superclass uses them
163+
using Django's Syndication framework.
164+
Docs: https://docs.djangoproject.com/en/4.2/ref/contrib/syndication/
165+
"""
166+
167+
feed_type = Rss201rev2Feed
168+
title = "Python Releases"
169+
description = "Latest Python releases from Python.org"
170+
171+
@staticmethod
172+
def link() -> str:
173+
"""Return the URL to the main downloads page."""
174+
return reverse("downloads:download")
175+
176+
def items(self) -> list[dict[str, Any]]:
177+
"""Return the latest Python releases."""
178+
return Release.objects.filter(is_published=True).order_by("-release_date")[:10]
179+
180+
def item_title(self, item: Release) -> str:
181+
"""Return the release name as the item title."""
182+
return item.name
183+
184+
def item_description(self, item: Release) -> str:
185+
"""Return the release version and release date as the item description."""
186+
return f"Version: {item.version}, Release Date: {item.release_date}"
187+
188+
def item_pubdate(self, item: Release) -> datetime | None:
189+
"""Return the release date as the item publication date."""
190+
if item.release_date:
191+
if timezone.is_naive(item.release_date):
192+
return timezone.make_aware(item.release_date)
193+
return item.release_date
194+
return None
195+
196+
def item_guid(self, item: Release) -> str:
197+
"""Return a unique ID for the item based on DB record."""
198+
return str(item.pk)

0 commit comments

Comments
 (0)