diff --git a/config/urls.py b/config/urls.py index be05ffa8..57cde8b3 100755 --- a/config/urls.py +++ b/config/urls.py @@ -58,6 +58,7 @@ from users.views import ( UserAvatar, ) from versions.api import ImportVersionsView, VersionViewSet +from versions.feeds import AtomVersionFeed, RSSVersionFeed from versions.views import VersionCurrentReleaseDetail, VersionDetail router = routers.SimpleRouter() @@ -72,6 +73,8 @@ urlpatterns = ( path("", HomepageView.as_view(), name="home"), path("homepage-beta/", HomepageBetaView.as_view(), name="home-beta"), path("admin/", admin.site.urls), + path("feed/downloads.rss", RSSVersionFeed(), name="downloads_feed_rss"), + path("feed/downloads.atom", AtomVersionFeed(), name="downloads_feed_atom"), path( "accounts/social/signup/", CustomSocialSignupViewView.as_view(), diff --git a/docs/README.md b/docs/README.md index ae920412..11397249 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,8 +5,11 @@ - [Development Setup Notes](./development_setup_notes.md) - [Environment Variables](./env_vars.md) - [Example Files](./examples/README.md) - Contains samples of `libraries.json`. `.gitmodules`, and other files that Boost data depends on -- [User Management](./user_management.md) - Describes how we allow authors and maintainers to "claim" the accounts that we create for them as part of the library upload process, and how to prevent users from updating their own profile photos. +- [Hosting](./hosting/README.md) +- [Mailman](./mailman/README.md) - [Management Commands](./commands.md) -- [Retrieving Static Content from the Boost Amazon S3 Bucket](./static_content.md) -- [Syncing Data about Boost Versions and Libraries with GitHub](./syncing_data_with_github.md) - [News and Moderation](./news.md) +- [Retrieving Static Content from the Boost Amazon S3 Bucket](./static_content.md) +- [RSS Feeds](./rss_feeds.md) +- [Syncing Data about Boost Versions and Libraries with GitHub](./syncing_data_with_github.md) +- [User Management](./user_management.md) - Describes how we allow authors and maintainers to "claim" the accounts that we create for them as part of the library upload process, and how to prevent users from updating their own profile photos. diff --git a/docs/rss_feeds.md b/docs/rss_feeds.md new file mode 100644 index 00000000..03da70b9 --- /dev/null +++ b/docs/rss_feeds.md @@ -0,0 +1,10 @@ +# RSS Feeds + +## Releases RSS Feed + +The `RSSVersionFeed` class controls the RSS feed for new Boost releases. + +**URLS**: + +- `/feed/downloads.rss` to replicate the original URL +- `/feed/downloads.atom` to publish an Atom version diff --git a/templates/versions/detail.html b/templates/versions/detail.html index cb0343bf..aa5f2b5b 100644 --- a/templates/versions/detail.html +++ b/templates/versions/detail.html @@ -15,6 +15,7 @@
{{ heading }} ({{ version.display_name }}) + RSS Feed
diff --git a/versions/feeds.py b/versions/feeds.py new file mode 100644 index 00000000..a3d62f0b --- /dev/null +++ b/versions/feeds.py @@ -0,0 +1,48 @@ +from datetime import datetime +from django.contrib.syndication.views import Feed +from django.utils.feedgenerator import Atom1Feed +from django.utils.timezone import make_aware, utc + +from core.models import RenderedContent +from .models import Version + + +class RSSVersionFeed(Feed): + """An RSS feed for Boost releases""" + + title = "Downloads" + link = "/downloads/" + description = "Recent downloads for Boost C++ Libraries." + + def items(self): + return ( + Version.objects.active().filter(full_release=True).order_by("-name")[:100] + ) + + def item_pubdate(self, item): + """Returns the release date as a timezone-aware datetime object""" + release_date = item.release_date + if release_date: + datetime_obj = datetime.combine(release_date, datetime.min.time()) + aware_datetime_obj = make_aware(datetime_obj, timezone=utc) + return aware_datetime_obj + + def item_description(self, item): + """Return the Release Notes in the description field if they are present.""" + release_notes = RenderedContent.objects.filter( + cache_key=item.release_notes_cache_key + ).first() + if release_notes: + return release_notes.content_html + return + + def item_title(self, item): + return f"Version {item.display_name}" + + +class AtomVersionFeed(RSSVersionFeed): + """The Atom feed version of the main feed, which enables + the extra fields like `pubdate` + """ + + feed_type = Atom1Feed diff --git a/versions/models.py b/versions/models.py index c66d0354..d6d6e548 100755 --- a/versions/models.py +++ b/versions/models.py @@ -1,5 +1,6 @@ import re from django.db import models +from django.urls import reverse from django.utils.functional import cached_property from django.utils.text import slugify @@ -43,6 +44,9 @@ class Version(models.Model): self.slug = self.get_slug() return super(Version, self).save(*args, **kwargs) + def get_absolute_url(self): + return reverse("release-detail", args=[str(self.slug)]) + def get_slug(self): if self.slug: return self.slug diff --git a/versions/tests/test_feeds.py b/versions/tests/test_feeds.py new file mode 100644 index 00000000..14fbcc32 --- /dev/null +++ b/versions/tests/test_feeds.py @@ -0,0 +1,56 @@ +from datetime import datetime +from django.utils.timezone import make_aware, utc +from ..feeds import RSSVersionFeed, AtomVersionFeed + + +def test_items(version, old_version): + feed = RSSVersionFeed() + items = feed.items() + assert len(items) == 2 + # Assert sorting + assert items[0] == version + # Assert all versions are present + assert old_version in items + + +def test_item_pubdate(version): + feed = RSSVersionFeed() + expected_datetime = make_aware( + datetime.combine(version.release_date, datetime.min.time()), timezone=utc + ) + assert feed.item_pubdate(version) == expected_datetime + + +def test_item_description_with_release_notes(version, rendered_content): + rendered_content.cache_key = version.release_notes_cache_key + rendered_content.save() + feed = RSSVersionFeed() + assert feed.item_description(version) == rendered_content.content_html + + +def test_item_description_without_release_notes(version): + feed = RSSVersionFeed() + assert feed.item_description(version) is None + + +def test_item_title(version): + feed = RSSVersionFeed() + assert feed.item_title(version) == f"Version {version.display_name}" + + +def test_items_atom(version, old_version): + feed = AtomVersionFeed() + items = feed.items() + assert len(items) == 2 + # Assert sorting + assert items[0] == version + # Assert all versions are present + assert old_version in items + + +def test_item_pubdate_atom(version): + feed = AtomVersionFeed() + expected_datetime = make_aware( + datetime.combine(version.release_date, datetime.min.time()), timezone=utc + ) + assert feed.item_pubdate(version) == expected_datetime diff --git a/versions/tests/test_models.py b/versions/tests/test_models.py index b390c75d..857663d3 100644 --- a/versions/tests/test_models.py +++ b/versions/tests/test_models.py @@ -93,3 +93,8 @@ def test_stripped_boost_url_slug(slug, expected, version): version.save() version.refresh_from_db() assert version.stripped_boost_url_slug == expected + + +def test_get_absolute_url(version): + expected_url = f"/releases/{version.slug}/" + assert version.get_absolute_url() == expected_url