Skip to content

Commit

Permalink
refactor: add more podcast queryset methods
Browse files Browse the repository at this point in the history
  • Loading branch information
danjac committed Aug 29, 2024
1 parent 3fd2caf commit 607e857
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 39 deletions.
2 changes: 1 addition & 1 deletion radiofeed/feedparser/management/commands/parse_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def handle(self, *args, **options) -> None:
def _get_scheduled_podcasts(self, limit: int) -> QuerySet[Podcast]:
return (
scheduler.get_scheduled_podcasts()
.active()
.alias(subscribers=Count("subscriptions"))
.filter(active=True)
.order_by(
F("subscribers").desc(),
F("promoted").desc(),
Expand Down
23 changes: 9 additions & 14 deletions radiofeed/podcasts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from typing import TYPE_CHECKING

from django.contrib import admin
from django.db.models import Count, Exists, OuterRef, QuerySet
from django.db.models import Count, QuerySet
from django.template.defaultfilters import timesince, timeuntil
from django.utils import timezone

from radiofeed.fast_count import FastCountAdminMixin
from radiofeed.feedparser import scheduler
from radiofeed.podcasts.models import Category, Podcast, Subscription
from radiofeed.podcasts.models import Category, Podcast

if TYPE_CHECKING: # pragma: no cover
from django.http import HttpRequest
Expand Down Expand Up @@ -55,9 +55,9 @@ def queryset(self, request: HttpRequest, queryset: QuerySet[Podcast]):
"""Returns filtered queryset."""
match self.value():
case "yes":
return queryset.filter(active=True)
return queryset.active()
case "no":
return queryset.filter(active=False)
return queryset.inactive()
case _:
return queryset

Expand Down Expand Up @@ -105,9 +105,9 @@ def queryset(
"""Returns filtered queryset."""
match self.value():
case "yes":
return queryset.filter(pub_date__isnull=False)
return queryset.published()
case "no":
return queryset.filter(pub_date__isnull=True)
return queryset.unpublished()
case _:
return queryset

Expand All @@ -128,7 +128,7 @@ def queryset(
self, request: HttpRequest, queryset: QuerySet[Podcast]
) -> QuerySet[Podcast]:
"""Returns filtered queryset."""
return queryset.filter(promoted=True) if self.value() == "yes" else queryset
return queryset.promoted() if self.value() == "yes" else queryset


class PrivateFilter(admin.SimpleListFilter):
Expand All @@ -147,7 +147,7 @@ def queryset(
self, request: HttpRequest, queryset: QuerySet[Podcast]
) -> QuerySet[Podcast]:
"""Returns filtered queryset."""
return queryset.filter(private=True) if self.value() == "yes" else queryset
return queryset.private() if self.value() == "yes" else queryset


class SubscribedFilter(admin.SimpleListFilter):
Expand All @@ -168,12 +168,7 @@ def queryset(
"""Returns filtered queryset."""

if self.value() == "yes":
return queryset.annotate(
has_subscribers=Exists(
Subscription.objects.filter(podcast=OuterRef("pk"))
)
).filter(has_subscribers=True)

return queryset.has_subscribed()
return queryset


Expand Down
2 changes: 1 addition & 1 deletion radiofeed/podcasts/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def send_recommendations_email(
# include recommended + promoted

podcasts = (
Podcast.objects.has_episodes()
Podcast.objects.published()
.public()
.annotate(
relevance=Coalesce(
Expand Down
32 changes: 29 additions & 3 deletions radiofeed/podcasts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ def search(self, search_term: str) -> models.QuerySet[Category]:
return self.filter(name__icontains=value)
return self.none()

def has_available_podcasts(self) -> models.QuerySet[Category]:
def with_available_podcasts(self) -> models.QuerySet[Category]:
"""Return only categories with public non-empty podcasts."""
return self.annotate(
has_podcasts=models.Exists(
Podcast.objects.filter(
categories=models.OuterRef("pk"),
)
.has_episodes()
.published()
.public()
)
).filter(has_podcasts=True)
Expand Down Expand Up @@ -116,14 +116,30 @@ def search(self, search_term: str) -> models.QuerySet[Podcast]:
)
return qs.annotate(exact_match=models.Value(0))

def has_episodes(self) -> models.QuerySet[Podcast]:
def active(self) -> models.QuerySet[Podcast]:
"""Returns podcasts still active i.e. can be updated from RSS feed."""
return self.filter(active=True)

def inactive(self) -> models.QuerySet[Podcast]:
"""Returns podcasts no longer active."""
return self.filter(active=False)

def published(self) -> models.QuerySet[Podcast]:
"""Return only podcasts with episodes.
Note: we just need to check `pub_date` as this is only set by the feed parser
from the latest episode date, so if the podcast has no episodes this will be NULL.
"""
return self.filter(pub_date__isnull=False)

def unpublished(self) -> models.QuerySet[Podcast]:
"""Return only podcasts without episodes.
Note: we just need to check `pub_date` as this is only set by the feed parser
from the latest episode date, so if the podcast has no episodes this will be NULL.
"""
return self.filter(pub_date__isnull=True)

def public(self) -> models.QuerySet[Podcast]:
"""Returns all non-private podcasts."""
return self.filter(private=False)
Expand All @@ -136,6 +152,16 @@ def promoted(self) -> models.QuerySet[Podcast]:
"""Returns all promoted podcasts."""
return self.filter(promoted=True)

def has_subscribed(self) -> models.QuerySet[Podcast]:
"""Returns podcasts with at least one subscription."""
return self.annotate(
is_subscribed=models.Exists(
Subscription.objects.filter(
podcast=models.OuterRef("pk"),
)
)
).filter(is_subscribed=True)

def subscribed(self, user: User) -> models.QuerySet[Podcast]:
"""Returns podcasts user is subscribed to."""
return self.annotate(
Expand Down
54 changes: 42 additions & 12 deletions radiofeed/podcasts/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ def test_search(self, category):
assert Category.objects.search("testing").count() == 1

@pytest.mark.django_db
def test_has_available_podcasts_none(self, category):
assert Category.objects.has_available_podcasts().exists() is False
def test_with_available_podcasts_none(self, category):
assert Category.objects.with_available_podcasts().exists() is False

@pytest.mark.django_db
def test_has_available_podcasts_no_public_podcasts(self, category):
category.podcasts.add(PodcastFactory(private=True, pub_date=timezone.now()))
assert Category.objects.has_available_podcasts().exists() is False
def test_with_available_podcasts_true(self, category):
category.podcasts.add(PodcastFactory(private=False, pub_date=timezone.now()))
assert Category.objects.with_available_podcasts().exists() is True

@pytest.mark.django_db
def test_has_available_podcasts_has_public_podcasts(self, category):
category.podcasts.add(PodcastFactory(private=False, pub_date=timezone.now()))
assert Category.objects.has_available_podcasts().exists() is True
def test_with_available_podcasts_false(self, category):
category.podcasts.add(PodcastFactory(private=True, pub_date=timezone.now()))
assert Category.objects.with_available_podcasts().exists() is False


class TestCategoryModel:
Expand Down Expand Up @@ -126,14 +126,44 @@ def test_compare_exact_and_partial_matches_in_search(self):
assert second.exact_match == 0

@pytest.mark.django_db
def test_has_episodes_true(self):
def test_published_true(self):
PodcastFactory(pub_date=timezone.now())
assert Podcast.objects.has_episodes().exists() is True
assert Podcast.objects.published().exists() is True

@pytest.mark.django_db
def test_has_episodes_false(self):
def test_published_false(self):
PodcastFactory(pub_date=None)
assert Podcast.objects.has_episodes().exists() is False
assert Podcast.objects.published().exists() is False

@pytest.mark.django_db
def test_unpublished_true(self):
PodcastFactory(pub_date=None)
assert Podcast.objects.unpublished().exists() is True

@pytest.mark.django_db
def test_unpublished_false(self):
PodcastFactory(pub_date=timezone.now())
assert Podcast.objects.unpublished().exists() is False

@pytest.mark.django_db
def test_active_true(self):
PodcastFactory(active=True)
assert Podcast.objects.active().exists() is True

@pytest.mark.django_db
def test_active_false(self):
PodcastFactory(active=False)
assert Podcast.objects.active().exists() is False

@pytest.mark.django_db
def test_inactive_true(self):
PodcastFactory(active=False)
assert Podcast.objects.inactive().exists() is True

@pytest.mark.django_db
def test_inactive_false(self):
PodcastFactory(active=True)
assert Podcast.objects.inactive().exists() is False

@pytest.mark.django_db
def test_private_true(self):
Expand Down
2 changes: 1 addition & 1 deletion radiofeed/podcasts/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def test_no_episodes(self, client, auth_user, podcast):
assert response.status_code == http.HTTPStatus.NOT_FOUND

@pytest.mark.django_db
def test_has_episodes(self, client, auth_user, episode):
def test_published(self, client, auth_user, episode):
response = client.get(episode.podcast.get_latest_episode_url())
assert response.url == episode.get_absolute_url()

Expand Down
6 changes: 3 additions & 3 deletions radiofeed/podcasts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def similar(
@login_required
def category_list(request: HttpRequest) -> TemplateResponse:
"""List all categories containing podcasts."""
categories = Category.objects.has_available_podcasts().order_by("name")
categories = Category.objects.with_available_podcasts().order_by("name")

if request.search:
categories = categories.search(request.search.value)
Expand All @@ -242,7 +242,7 @@ def category_detail(
Podcasts can also be searched.
"""
category = get_object_or_404(Category, pk=category_id)
podcasts = category.podcasts.has_episodes().public()
podcasts = category.podcasts.published().public()

podcasts = (
podcasts.search(request.search.value).order_by(
Expand Down Expand Up @@ -361,7 +361,7 @@ def _get_podcast_or_404(podcast_id: int, **kwargs) -> Podcast:


def _get_podcasts() -> QuerySet[Podcast]:
return Podcast.objects.has_episodes()
return Podcast.objects.published()


def _render_subscribe_action(
Expand Down
File renamed without changes.
5 changes: 1 addition & 4 deletions radiofeed/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ def export_podcast_feeds(request: HttpRequest) -> TemplateResponse:
"""Download OPML document containing public feeds from user's subscriptions."""

podcasts = (
Podcast.objects.subscribed(request.user)
.has_episodes()
.public()
.order_by("title")
Podcast.objects.subscribed(request.user).published().public().order_by("title")
)

return TemplateResponse(
Expand Down

0 comments on commit 607e857

Please sign in to comment.