Skip to content
Snippets Groups Projects
Commit 0fed711d authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt :koala:
Browse files

refactor: use django_filters FilterSet for APIShowViewSet

This change re-implements all existing collection filters for the
APIShowViewSet with a FilterSet. No breaking changes are expected,
though there are some changes in semantics:

* The owner, host, musicfocus, language, category, and topic
  filters now accept multiple values (i.e. ?category=2,3)
* The owner, host, musicfocus, language, category, topic, and type
  filter values are now validated and may be rejected as invalid if
  the referenced object does not exist.
parent 53560190
No related branches found
No related tags found
1 merge request!20refactor collection filters with django_filters
from django_filters import rest_framework as filters
from django_filters import widgets
from django.contrib.auth.models import User
from django.db.models import Q, QuerySet
from django.utils import timezone
from program import models
class StaticFilterHelpTextMixin:
@classmethod
def filter_for_field(cls, field, field_name, lookup_expr=None):
_filter = super().filter_for_field(field, field_name, lookup_expr)
if "help_text" not in _filter.extra:
help_texts = getattr(cls.Meta, "help_texts", {})
_filter.extra["help_text"] = help_texts.get(field_name, "")
return _filter
class ModelMultipleChoiceFilter(filters.ModelMultipleChoiceFilter):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", widgets.CSVWidget())
kwargs["lookup_expr"] = "in"
super().__init__(*args, **kwargs)
def get_filter_predicate(self, v):
# There is something wrong with using ModelMultipleChoiceFilter
# along the CSVWidget that causes lookups to fail.
# May be related to: https://github.com/carltongibson/django-filter/issues/1103
return super().get_filter_predicate([v.pk])
class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
active = filters.BooleanFilter(
field_name="is_active",
method="filter_active",
help_text=(
"Return only currently running shows if true or past or upcoming shows if false.",
),
)
host = ModelMultipleChoiceFilter(
queryset=models.Host.objects.all(),
field_name="hosts",
help_text="Return only shows assigned to the given host(s).",
)
# TODO: replace `musicfocus` with `music_focus` when dashboard is updated
musicfocus = ModelMultipleChoiceFilter(
queryset=models.MusicFocus.objects.all(),
field_name="music_focus",
help_text="Return only shows with given music focus(es).",
)
owner = ModelMultipleChoiceFilter(
queryset=User.objects.all(),
field_name="owners",
help_text="Return only shows that belong to the given owner(s).",
)
category = ModelMultipleChoiceFilter(
queryset=models.Category.objects.all(),
help_text="Return only shows of the given category or categories.",
)
language = ModelMultipleChoiceFilter(
queryset=models.Language.objects.all(),
help_text="Return only shows of the given language(s).",
)
topic = ModelMultipleChoiceFilter(
queryset=models.Topic.objects.all(),
help_text="Return only shows of the given topic(s).",
)
public = filters.BooleanFilter(
field_name="is_public",
help_text="Return only shows that are public/non-public.",
)
def filter_active(self, queryset: QuerySet, name: str, value: bool):
# Filter currently running shows
# Get currently running schedules to filter by first
# For single dates we test if there'll be one in the future (and ignore the until date)
# TODO: Really consider first_date? (=currently active, not just upcoming ones)
# Add limit for future?
show_ids = (
models.Schedule.objects.filter(
Q(
rrule_id__gt=1,
first_date__lte=timezone.now(),
last_date__gte=timezone.now(),
)
| Q(rrule_id=1, first_date__gte=timezone.now())
)
.distinct()
.values_list("show_id", flat=True)
)
if value:
# Filter active shows based on timeslots as well as on the is_active flag
# Even if there are future timeslots but is_active=True the show will be considered as
# inactive
return queryset.filter(id__in=show_ids, is_active=True)
else:
return queryset.exclude(id__in=show_ids, is_active=True)
class Meta:
model = models.Show
help_texts = {
"type": "Return only shows of a given type.",
}
fields = [
"active",
"category",
"host",
"language",
"musicfocus",
"owner",
"public",
"topic",
"type",
]
...@@ -27,11 +27,11 @@ from rest_framework.pagination import LimitOffsetPagination ...@@ -27,11 +27,11 @@ from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response from rest_framework.response import Response
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from program import filters
from program.models import ( from program.models import (
Category, Category,
FundingCategory, FundingCategory,
...@@ -264,106 +264,21 @@ class APIUserViewSet(viewsets.ModelViewSet): ...@@ -264,106 +264,21 @@ class APIUserViewSet(viewsets.ModelViewSet):
class APIShowViewSet(viewsets.ModelViewSet): class APIShowViewSet(viewsets.ModelViewSet):
""" """
/shows/ returns all shows (GET, POST) Returns a list of available shows.
/shows/?active=true returns all active shows (= currently running) (GET) Only superusers may add and delete shows.
/shows/?active=false returns all inactive shows (= past or upcoming) (GET)
/shows/?public=true returns all public shows (GET)
/shows/?public=false returns all non-public shows (GET)
/shows/?host={host_pk} returns shows assigned to a given host (GET)
/shows/?owner={owner_pk} returns shows of a given owner (GET)
/shows/?language={language_pk} returns shows in a given language (GET)
/shows/?type={type_pk} returns shows of a given type (GET)
/shows/?category={category_pk} returns shows of a given category (GET)
/shows/?topic={topic_pk} returns shows of a given topic (GET)
/shows/?musicfocus={musicfocus_pk} returns shows of a given music focus (GET)
/shows/{pk|slug} retrieves or updates (if owned) a single show (GET, PUT).
Only superusers may add and delete shows
""" """
queryset = Show.objects.none() queryset = Show.objects.all()
serializer_class = ShowSerializer serializer_class = ShowSerializer
permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly]
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
filterset_class = filters.ShowFilterSet
def get_queryset(self):
shows = Show.objects.all()
# Filters
if (
self.request.query_params.get("active") == "true"
or self.request.query_params.get("active") == "false"
):
# Filter currently running shows
# Get currently running schedules to filter by first
# For single dates we test if there'll be one in the future (and ignore the until date)
# TODO: Really consider first_date? (=currently active, not just upcoming ones)
# Add limit for future?
show_ids = (
Schedule.objects.filter(
Q(
rrule_id__gt=1,
first_date__lte=timezone.now(),
last_date__gte=timezone.now(),
)
| Q(rrule_id=1, first_date__gte=timezone.now())
)
.distinct()
.values_list("show_id", flat=True)
)
# Filter active shows based on timeslots as well as on the is_active flag
# Even if there are future timeslots but is_active=True the show will be considered as
# inactive
shows = Show.objects.filter(id__in=show_ids, is_active=True)
if self.request.query_params.get("active") == "false":
# Return all shows except those which are running
shows = Show.objects.exclude(id__in=show_ids, is_active=True)
if self.request.query_params.get("public") == "true":
# Return all public shows
shows = shows.filter(is_public=True)
if self.request.query_params.get("public") == "false":
# Return all public shows
shows = shows.filter(is_public=False)
if owner := self.request.query_params.get("owner"):
if owner != "undefined":
shows = shows.filter(owners__in=[int(owner)])
if host := self.request.query_params.get("host"):
if host != "undefined":
shows = shows.filter(hosts__in=[int(host)])
if language := self.request.query_params.get("language"):
if language != "undefined":
shows = shows.filter(language__in=[int(language)])
if type_ := self.request.query_params.get("type"):
if type_ != "undefined":
shows = shows.filter(type__in=[int(type_)])
if category := self.request.query_params.get("category"):
if category != "undefined":
shows = shows.filter(category__in=[int(category)])
if topic := self.request.query_params.get("topic"):
if topic != "undefined":
shows = shows.filter(topic__in=[int(topic)])
# TODO: replace `musicfocus` with `music_focus` when dashboard is updated
if music_focus := self.request.query_params.get("musicfocus"):
if music_focus != "undefined":
shows = shows.filter(music_focus__in=[int(music_focus)])
return shows
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" """
Create a show Create a show.
Only superusers may create a show
Only superusers may create a show.
""" """
if not request.user.is_superuser: if not request.user.is_superuser:
...@@ -396,8 +311,9 @@ class APIShowViewSet(viewsets.ModelViewSet): ...@@ -396,8 +311,9 @@ class APIShowViewSet(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
""" """
Update a show Update a show.
Common users may only update shows they own
Common users may only update shows they own.
""" """
pk = get_values(self.kwargs, "pk") pk = get_values(self.kwargs, "pk")
...@@ -423,8 +339,9 @@ class APIShowViewSet(viewsets.ModelViewSet): ...@@ -423,8 +339,9 @@ class APIShowViewSet(viewsets.ModelViewSet):
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
""" """
Delete a show Delete a show.
Only superusers may delete shows
Only superusers may delete shows.
""" """
if not request.user.is_superuser: if not request.user.is_superuser:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment