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

RSS for shelves #3013

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
856737e
working rss feed for shelves
Sep 29, 2023
4ae0dbd
add a failing test for rss feeds for shelves
Sep 29, 2023
c142e38
fix linting issue
Oct 1, 2023
f665aea
fixed method without docstring
Oct 1, 2023
339298c
Mock activitystreams add_book_statuses_task
Nov 5, 2023
d3d5f1b
remove duplicate sitesettings error
Nov 5, 2023
7a25869
Merge branch 'main' into rss-for-shelves
jaschaurbach Jan 20, 2024
7b388ae
Sort RSS Feed by Shelved date
mattkatz Mar 13, 2024
3754af7
/rss shouldn't be optional in the rss route
mattkatz Mar 13, 2024
a2e41fa
/rss shouldn't be optional in the rss route
mattkatz Mar 13, 2024
975c3ba
Correct docstring on rss feed
mattkatz Mar 13, 2024
e3ca543
Remove extraneous function
Mar 13, 2024
5b8e083
use privacy__in to respect shelf privacy settings
Mar 13, 2024
63d6486
Merge branch 'main' into rss-for-shelves
mattkatz Mar 19, 2024
6976cb4
add user name to shelf rss feed title
mattkatz Mar 19, 2024
cd29b44
improve shelf rss description
mattkatz Mar 19, 2024
1d8bd2b
add translation usage
Mar 19, 2024
09d857e
support same identifiers as book page in rss
Mar 19, 2024
1e14d63
Missing brackets and correct blocktrans usage
dato Mar 19, 2024
5340ed3
Drop `default` filter in favor of per-metadata item conditionals
dato Mar 19, 2024
8dc412c
remove pure_name since it only applies to statuses
Apr 14, 2024
a0d15cc
Don't show a colon if there is no author_text
Apr 14, 2024
74e2103
handle cases where edition has no author
Apr 14, 2024
98724df
use display_name to avoid name rendering as None
Apr 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions bookwyrm/templates/rss/edition.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{obj.title}} by {{obj.author_text}}
dato marked this conversation as resolved.
Show resolved Hide resolved
{{obj.description|default:""}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be a blocker because it seems to be a bit difficult to manage embedded HTML in the Django syndication framework, but I noticed this just runs on without line breaks so it renders like Title of Book by Jo Bloggs Here is a description of the book.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what to do here.

If I could figure out how to trigger a cdata block, we could do something to embed new lines.
This seems like an approach someone got working, if it makes sense to you, I can take a swing at it.
https://nemecek.be/blog/33/how-to-create-rss-feed-with-html-content-in-django

For my purposes, I'm very interested in getting the book ISBN and other identifiers in as raw a format as possible, but I'd like to make sure this feed is useful and looks good for humans in a feed reader.

When I look at how other book tracking sites handle this, they use the cdata block pretty liberally.
They also just introduce custom elements like isbn and asin directly in the feed. Which doesn't seem valid xml but it seems not to blow anything up.
I'm not ready to dedicate myself just yet to implementing a custom feed generator as described in https://docs.djangoproject.com/en/5.0/ref/contrib/syndication/#custom-feed-generators

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw @hughrun - this is listed as a blocking change I think - assuming I'm not misreading githubs UI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look at this shortly.

{% if obj.description %}ISBN: {{item.isbn_10|default:""}}{% endif %}
{% if obj.description %}ISBN13: {{item.isbn_13}}{% endif %}
24 changes: 24 additions & 0 deletions bookwyrm/tests/views/test_rss_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,27 @@ def test_rss_quotation_only(self, *_):
self.assertEqual(result.status_code, 200)

self.assertIn(b"a sickening sense", result.content)

def test_rss_shelf(self, *_):
"""load the rss feed of a shelf"""
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"
), patch("bookwyrm.activitystreams.add_book_statuses_task.delay"):
# make the shelf
shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user
)
# put the shelf on the book
models.ShelfBook.objects.create(
book=self.book,
shelf=shelf,
user=self.local_user,
)
view = rss_feed.RssShelfFeed()
request = self.factory.get("/user/books/test-shelf/rss")
request.user = self.local_user
result = view(
request, username=self.local_user.username, shelf_identifier="test-shelf"
)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Example Edition", result.content)
10 changes: 10 additions & 0 deletions bookwyrm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,11 +572,21 @@
views.Shelf.as_view(),
name="shelf",
),
re_path(
rf"^{USER_PATH}/(shelf|books)/(?P<shelf_identifier>[\w-]+)(/rss)?/?$",
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
views.rss_feed.RssShelfFeed(),
name="shelf-rss",
),
re_path(
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)(.json)?/?$",
views.Shelf.as_view(),
name="shelf",
),
re_path(
rf"^{LOCAL_USER_PATH}/(books|shelf)/(?P<shelf_identifier>[\w-]+)(/rss)?/?$",
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
views.rss_feed.RssShelfFeed(),
name="shelf-rss",
),
re_path(r"^create-shelf/?$", views.create_shelf, name="shelf-create"),
re_path(r"^delete-shelf/(?P<shelf_id>\d+)/?$", views.delete_shelf),
re_path(r"^shelve/?$", views.shelve),
Expand Down
64 changes: 64 additions & 0 deletions bookwyrm/views/rss_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.syndication.views import Feed
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
from django.shortcuts import get_object_or_404
from ..models import Review, Quotation, Comment

from .helpers import get_user_from_username
Expand Down Expand Up @@ -177,3 +178,66 @@ def item_link(self, item):
def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date


class RssShelfFeed(Feed):
"""serialize a shelf activity in rss"""

description_template = "rss/edition.html"

def item_title(self, item):
"""render the item title"""
if hasattr(item, "pure_name") and item.pure_name:
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
return item.pure_name
authors = item.authors
authors.display_name = f"{item.author_text}:"
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
template = get_template("rss/title.html")
return template.render({"user": authors, "item_title": item.title}).strip()

def get_object(
self, request, shelf_identifier, username
): # pylint: disable=arguments-differ
"""the user who's posts get serialized"""
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
user = get_user_from_username(request.user, username)
# always get privacy, don't support rss over anything private
# Shelf.privacy_filter(request.user).filter(user=user).all()

# TODO: get the SHELF of the object
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
mattkatz marked this conversation as resolved.
Show resolved Hide resolved
shelf.raise_visible_to_user(request.user)
return shelf

def link(self, obj):
"""link to the shelf"""
return obj.local_path

def title(self, obj):
"""title of the rss feed entry"""
return _(f"Books added to {obj.name}")
mattkatz marked this conversation as resolved.
Show resolved Hide resolved

def items(self, obj):
"""the user's activity feed"""
return obj.books.order_by("-published_date")[:10]
mattkatz marked this conversation as resolved.
Show resolved Hide resolved

def item_link(self, item):
"""link to the status"""
return item.local_path

def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date

def description(self, obj):
"""description of the shelf including the shelf name and user."""
# if there's a description, lets add it. Not everyone puts a description in.
desc = ""
if obj.description:
desc = f" {obj.description}"
return f"Books added to the '{obj.name}' shelf for {obj.user.name}.{desc}"
mattkatz marked this conversation as resolved.
Show resolved Hide resolved

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# TODO: gotta check that this is the SHELF user!
context["user"] = kwargs["request"].user
context["shelf"] = kwargs["obj"]
return context
mattkatz marked this conversation as resolved.
Show resolved Hide resolved