diff --git a/downloads/urls.py b/downloads/urls.py index d64f0a1ad..f553caeaa 100644 --- a/downloads/urls.py +++ b/downloads/urls.py @@ -9,4 +9,5 @@ path('release//', views.DownloadReleaseDetail.as_view(), name='download_release_detail'), path('/', views.DownloadOSList.as_view(), name='download_os_list'), path('', views.DownloadHome.as_view(), name='download'), + path("feed.rss", views.ReleaseFeed(), name="feed"), ] diff --git a/downloads/views.py b/downloads/views.py index 746845402..a3255425d 100644 --- a/downloads/views.py +++ b/downloads/views.py @@ -1,10 +1,22 @@ +import logging +from typing import Any + +import requests +from datetime import datetime + +from django.contrib.sites.shortcuts import get_current_site +from django.core.handlers.wsgi import WSGIRequest from django.db.models import Prefetch from django.urls import reverse 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 +import pytz from .models import OS, Release, ReleaseFile +logger = logging.getLogger(__name__) class DownloadLatestPython2(RedirectView): """ Redirect to latest Python 2 release """ @@ -147,3 +159,92 @@ 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: + """Return the URL to the main downloads page.""" + return reverse('downloads:download') + + def get_feed(self, obj: Any, request: WSGIRequest) -> Feed: + """Store the request object for later use.""" + self.request = request + return super().get_feed(obj, request) + + def items(self) -> list[dict[str, Any]]: + """Return the latest Python releases.""" + url = self.create_url("/api/v2/downloads/release/") + logger.info(f"Fetching releases from: {url}") + try: + return self._fetch_releases(url) + except requests.RequestException as e: + logger.error(f"Error fetching releases from API: {str(e)}") + except ValueError as e: + logger.error(f"Error parsing JSON from API response: {str(e)}") + except Exception as e: + logger.error(f"Unexpected error in items method: {str(e)}") + return [] + + @staticmethod + def _fetch_releases(url: str) -> list[dict[str, Any]]: + """Grabs the latest Python releases from API. + + + """ + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + + sorted_releases = sorted(data, key=lambda x: x["release_date"], reverse=True) + return sorted_releases[:10] + + def item_title(self, item: dict[str, Any]) -> str: + """Return the release name as the item title.""" + return item.get("name", "Unknown Release") + + def item_description(self, item: dict[str, Any]) -> str: + """Return the release version and release date as the item description.""" + version = item.get("version", "Unknown") + release_date = item.get("release_date", "Unknown") + return f"Version: {version}, Release Date: {release_date}" + + def item_link(self, item: dict[str, Any]) -> str: + """Return the URL to the release page on python.org.""" + return reverse("downloads:download_release_detail", args=[item.get("slug", "")]) + + @staticmethod + def item_pubdate(item: dict[str, Any]) -> datetime: + """Return the release date as the item publication date.""" + try: + release_date = datetime.strptime( + item.get("release_date", ""), "%Y-%m-%dT%H:%M:%SZ" + ) + return pytz.utc.localize(release_date) + except ValueError: + logger.error( + f"Invalid release date format for item: {item.get('name', 'Unknown')}" + ) + return pytz.utc.localize(datetime.now()) + + @staticmethod + def item_guid(item: dict[str, Any]) -> str: + """Return the release URI as the item GUID.""" + return item.get("resource_uri", "") + + def create_url(self, path: str) -> str: + """Create a full URL using the current site domain.""" + current_site = get_current_site(self.request) + scheme = "https" if self.request.is_secure() else "http" + return f"{scheme}://{current_site.domain}{path}"