Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#1612): add rss feed for latest downloads #2569

Merged
merged 17 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions downloads/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ def purge_fastly_download_pages(sender, instance, **kwargs):
if instance.is_published:
# Purge our common pages
purge_url('/downloads/')
purge_url('/downloads/feed.rss')
purge_url('/downloads/latest/python2/')
purge_url('/downloads/latest/python3/')
purge_url('/downloads/macos/')
Expand Down
42 changes: 42 additions & 0 deletions downloads/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,45 @@ def test_filter_release_file_delete_by_release(self):
headers={"authorization": self.Authorization}
)
self.assertEqual(response.status_code, 405)

class ReleaseFeedTests(BaseDownloadTests):
"""Tests for the downloads/feed.rss endpoint.

Content is ensured via setUp in BaseDownloadTests.
"""

url = reverse("downloads:feed")


def test_endpoint_reachable(self) -> None:
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_feed_content(self) -> None:
"""Ensure feed content is as expected.

Some things we want to check:
- Feed title, description, pubdate
- Feed items (releases) are in the correct order
- We get the expected number of releases (10)
"""
response = self.client.get(self.url)
content = response.content.decode()

self.assertIn("Python 2.7.5", content)
self.assertIn("Python 3.10", content)
# Published but hidden show up in the API and thus the feed
self.assertIn("Python 0.0.0", content)

# No unpublished releases
self.assertNotIn("Python 9.7.2", content)

# Pre-releases are shown
self.assertIn("Python 3.9.90", content)

def test_feed_item_count(self) -> None:
response = self.client.get(self.url)
content = response.content.decode()

# In BaseDownloadTests, we create 5 releases, 4 of which are published, 1 of those published are hidden..
self.assertEqual(content.count("<item>"), 4)
1 change: 1 addition & 0 deletions downloads/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
path('release/<slug:release_slug>/', views.DownloadReleaseDetail.as_view(), name='download_release_detail'),
path('<slug:slug>/', views.DownloadOSList.as_view(), name='download_os_list'),
path('', views.DownloadHome.as_view(), name='download'),
path("feed.rss", views.ReleaseFeed(), name="feed"),
JacobCoffee marked this conversation as resolved.
Show resolved Hide resolved
]
49 changes: 49 additions & 0 deletions downloads/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from typing import Any

from datetime import datetime

from django.db.models import Prefetch
from django.urls import reverse
from django.utils import timezone
from django.views.generic import DetailView, TemplateView, ListView, RedirectView
from django.http import Http404
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Rss201rev2Feed

from .models import OS, Release, ReleaseFile

Expand Down Expand Up @@ -147,3 +154,45 @@ def get_context_data(self, **kwargs):
)

return context


class ReleaseFeed(Feed):
"""Generate an RSS feed of the latest Python releases.

.. note:: It may seem like these are unused methods, but the superclass uses them
using Django's Syndication framework.
Docs: https://docs.djangoproject.com/en/4.2/ref/contrib/syndication/
"""

feed_type = Rss201rev2Feed
title = "Python Releases"
description = "Latest Python releases from Python.org"

@staticmethod
def link() -> str:
JacobCoffee marked this conversation as resolved.
Show resolved Hide resolved
"""Return the URL to the main downloads page."""
return reverse("downloads:download")

def items(self) -> list[dict[str, Any]]:
ewdurbin marked this conversation as resolved.
Show resolved Hide resolved
"""Return the latest Python releases."""
return Release.objects.filter(is_published=True).order_by("-release_date")[:10]
JacobCoffee marked this conversation as resolved.
Show resolved Hide resolved

def item_title(self, item: Release) -> str:
"""Return the release name as the item title."""
return item.name

def item_description(self, item: Release) -> str:
"""Return the release version and release date as the item description."""
return f"Version: {item.version}, Release Date: {item.release_date}"

def item_pubdate(self, item: Release) -> datetime | None:
"""Return the release date as the item publication date."""
if item.release_date:
if timezone.is_naive(item.release_date):
return timezone.make_aware(item.release_date)
return item.release_date
return None

def item_guid(self, item: Release) -> str:
"""Return a unique ID for the item based on DB record."""
return str(item.pk)