Skip to content
Draft
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
95 changes: 95 additions & 0 deletions activity/feeds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import uuid
from itertools import chain
from operator import attrgetter

from django.contrib.syndication.views import Feed
from django.urls import reverse
from django.utils.feedgenerator import Rss201rev2Feed


from activity.models import (
GeoreferenceGroup,
SitewideMilestone,
SubjectIntroduction,
UserMilestone,
CollectionIntroduction,
)

MAX_ITEMS = 50


class SitewideActivityFeed(Feed):
"""
Site-wide RSS feed showing the latest georeferences, subjects, and milestones.
"""
feed_type = Rss201rev2Feed
title = "Yesterdays - Site-wide Activity"
link = "/activity/" # Update with your main activity page URL
description = "The latest georeferences subjects, and milestones in Yesterdays."

def items(self):
# Fetch the most recent items of each activity type.
georefs = GeoreferenceGroup.objects.all().order_by('-ended_at')[:MAX_ITEMS]
user_milestones = UserMilestone.objects.all().order_by('-reached_at')[:MAX_ITEMS]
sitewide_milestones = SitewideMilestone.objects.all().order_by('-created_at')[:MAX_ITEMS]
subjects = SubjectIntroduction.objects.all().order_by('-reached_at')[:MAX_ITEMS]
collections = CollectionIntroduction.objects.all().order_by('-created_at')[:MAX_ITEMS]

# Chain them together and sort chronologically
combined = sorted(
chain(georefs, subjects, user_milestones, sitewide_milestones, collections),
key=attrgetter('created_at', 'reached_at', 'ended_at'),
reverse=True,
)
return combined[:MAX_ITEMS]

def item_title(self, item):
if isinstance(item, GeoreferenceGroup):
return f"Images georeferenced by {getattr(item, 'user', 'a user')}"
elif isinstance(item, SubjectIntroduction):
return f"New subject added: {item.subject.title}"
elif isinstance(item, UserMilestone):
return f"{item.user.username} reached {item.count} georeferences!"
elif isinstance(item, SitewideMilestone):
return f"Yesterdays reached {item.count} images georeferenced!"
elif isinstance(item, CollectionIntroduction):
return f"New collection added: {item.collection.name}"
return str(item)

def item_description(self, item):
if isinstance(item, GeoreferenceGroup):
count = getattr(item, 'count', 'Multiple')
return f"{count} images were recently georeferenced."
elif isinstance(item, SubjectIntroduction):
return "A new subject was introduced to the catalog."
elif isinstance(item, UserMilestone):
return "A user has reached a new georeferencing milestone."
elif isinstance(item, SitewideMilestone):
return "The community has reached a new site-wide milestone."
elif isinstance(item, CollectionIntroduction):
return "A new collection was introduced to the catalog."
return str(item)

def item_link(self, item):
# Default to method if the model has one. Covers Image, Subject, Collection
if hasattr(item, 'get_absolute_url'):
return item.get_absolute_url()
elif isinstance(item, SubjectIntroduction):
return item.subject.get_absolute_url()
elif isinstance(item, UserMilestone):
return reverse("user_profile", kwargs={"username": item.user.username})
elif isinstance(item, CollectionIntroduction):
return item.collection.get_absolute_url()

# GeoreferenceGroup and SitewideMilestone go nowhere?
return "/activity/"

def item_pubdate(self, item):
return getattr(item, 'created_at', None) or getattr(item, 'reached_at', None) or getattr(item, 'ended_at', None)

def item_guid(self, item):
"""
Set a random UUID for each item, so that the exact same session can appear in
multiple feeds if necessary and won't be filtered by RSS clients.
"""
return str(uuid.uuid4())
4 changes: 3 additions & 1 deletion activity/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from django.urls import path

from . import views
from activity import views
from activity.feeds import SitewideActivityFeed

app_name = "activity"

urlpatterns = [
path("", views.activity_feed, name="feed"),
path("feed/",SitewideActivityFeed(), name="site-feed")
]
38 changes: 38 additions & 0 deletions api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1108,3 +1108,41 @@ def test_cannot_revoke_other_users_consent(self):
)
self.assertEqual(resp.status_code, 404)
self.assertTrue(ApplicationConsent.objects.filter(pk=consent.pk).exists())


# ---------------------------------------------------------------------------
# RSS Feeds
# ---------------------------------------------------------------------------


class TestSitewideActivityFeed(ApiFixturesMixin, TestCase):
def test_feed_status_and_type(self):
resp = self.client.get("/activity/feed/")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp["Content-Type"], "application/rss+xml; charset=utf-8")

def test_feed_content(self):
resp = self.client.get("/activity/feed/")
content = resp.content.decode("utf-8")

self.assertIn('<rss version="2.0"', content)
self.assertIn("<title>Yesterdays - Site-wide Activity</title>", content)
# Based on the test fixtures, we should have a milestone in the activity feed
self.assertIn("milestone", content.lower())


class TestSubjectActivityFeed(ApiFixturesMixin, TestCase):
def test_feed_status_and_type(self):
resp = self.client.get(f"/subjects/{self.subject.slug}/feed/")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp["Content-Type"], "application/rss+xml; charset=utf-8")

def test_feed_content(self):
resp = self.client.get(f"/subjects/{self.subject.slug}/feed/")
content = resp.content.decode("utf-8")
self.assertIn('<rss version="2.0"', content)
self.assertIn(self.img1.title, content)

def test_feed_404(self):
resp = self.client.get("/subjects/fake-subject/feed/")
self.assertEqual(resp.status_code, 404)
54 changes: 54 additions & 0 deletions subjects/feeds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import uuid

from django.contrib.syndication.views import Feed
from django.shortcuts import get_object_or_404
from django.utils.feedgenerator import Rss201rev2Feed

from images.models import Image
from subjects.models import Subject

class SubjectActivityFeed(Feed):
"""
Subject-specific RSS feed showing new photos tagged with a given subject.
"""
feed_type = Rss201rev2Feed

def get_object(self, request, subject_slug):
# Grabs the subject object when the URL is requested
return get_object_or_404(Subject, slug=subject_slug)

def title(self, subject):
return f"Yesterdays - New images for {subject.name}"

def link(self, subject):
return subject.get_absolute_url()

def description(self, subject):
return f"Latest images tagged with: {subject.name}."

def items(self, subject):
# Assuming SubjectMapping links Image and Subject via a ForeignKey to Subject.
# The related lookup name (`subjectmapping__subject`) might need to be tweaked
# depending on your exact ForeignKey setup in SubjectMapping.
return Image.objects.filter(
subjectmapping__subject=subject,
).distinct().order_by('-created_at')[:50]

def item_title(self, image):
return f"New image tagged: {image.title}"

def item_description(self, image):
return image.description

def item_link(self, item):
return item.get_absolute_url()

def item_pubdate(self, image):
return image.created_at

def item_guid(self, subject):
"""
Set a random UUID for each item, so that the exact same session can appear in
multiple feeds if necessary and won't be filtered by RSS clients.
"""
return str(uuid.uuid4())
4 changes: 3 additions & 1 deletion subjects/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.urls import path

from . import views
from subjects import views
from subjects.feeds import SubjectActivityFeed

app_name = "subjects"

Expand All @@ -9,6 +10,7 @@
path("", views.browse_subjects, name="browse_subjects"),
path("map/", views.subjects_map, name="subjects_map"),
path("<slug:subject_slug>/", views.subject_detail, name="subject_detail"),
path("<slug:subject_slug>/feed/", SubjectActivityFeed(), name="subject_feed"),
path(
"<slug:subject_slug>/similar/",
views.find_similar_images_to_subject,
Expand Down