-
Ernesto Rico Schmidt authoredErnesto Rico Schmidt authored
filters.py 9.39 KiB
import datetime
from django_filters import rest_framework as filters
from django_filters import widgets
from django import forms
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 IntegerInFilter(filters.BaseInFilter):
class QueryArrayWidget(widgets.QueryArrayWidget):
# see: https://github.com/carltongibson/django-filter/issues/1047
def value_from_datadict(self, data, files, name):
new_data = {}
for key in data.keys():
if len(data.getlist(key)) == 1 and "," in data[key]:
new_data[key] = data[key]
else:
new_data[key] = data.getlist(key)
return super().value_from_datadict(new_data, files, name)
field_class = forms.IntegerField
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", self.QueryArrayWidget())
super().__init__(*args, **kwargs)
class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
active = filters.BooleanFilter(
field_name="is_active",
method="filter_active",
help_text=(
"Return only currently running shows (with timeslots in the future) if true "
"or past or upcoming shows if false."
),
)
host = IntegerInFilter(
field_name="hosts",
help_text="Return only shows assigned to the given host(s).",
)
music_focus = IntegerInFilter(
field_name="music_focus",
help_text="Return only shows with given music focus(es).",
)
music_focus__slug = filters.CharFilter(
field_name="music_focus", help_text="Return only shows with the give music focus slug."
)
owner = IntegerInFilter(
field_name="owners",
help_text="Return only shows that belong to the given owner(s).",
)
category = IntegerInFilter(
help_text="Return only shows of the given category or categories.",
)
category__slug = filters.CharFilter(
field_name="category", help_text="Return only shows of the given category slug."
)
language = IntegerInFilter(
help_text="Return only shows of the given language(s).",
)
topic = IntegerInFilter(
help_text="Return only shows of the given topic(s).",
)
topic__slug = filters.CharFilter(
field_name="topic", help_text="Return only shows of the given topic slug."
)
type = IntegerInFilter(
help_text="Return only shows of a given type.",
)
type__slug = filters.CharFilter(
field_name="type", help_text="Return only shows of the given type slug."
)
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(
# not "once" schedules with first_date in the past and last_date in the future
Q(
rrule__freq__gt=0,
first_date__lte=timezone.now(),
last_date__gte=timezone.now(),
)
# "once" schedules with first_date in the future
| Q(rrule__freq=0, 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)
@property
def qs(self):
# allow pagination query parameters in the GET request
fields = self.Meta.fields + ["limit", "offset"]
if any([key for key in self.request.GET.keys() if key not in fields]):
return None
else:
return super().qs
class Meta:
model = models.Show
fields = [
"active",
"category",
"host",
"language",
"music_focus",
"owner",
"public",
"topic",
"type",
]
class TimeSlotFilterSet(filters.FilterSet):
order = filters.OrderingFilter(
fields=[field.name for field in models.TimeSlot._meta.get_fields()]
)
surrounding = filters.DateTimeFilter(
method="filter_surrounding",
label="Return surrounding timeslots",
help_text=(
"Returns the 10 nearest timeslots around the specified datetime. "
"If specified without a datetime value the current date and time is assumed."
),
)
# The start/end filters will always be applied even if no query parameter has been set.
# This is because we enforce a value in the clean_start and clean_end methods
# of the filterset form.
start = filters.DateFilter(
method="filter_start",
help_text=(
"Only returns timeslots after that start on or after the specified date. "
"By default, this is set to the current date."
),
)
end = filters.DateFilter(
method="filter_end",
help_text=(
"Only returns timeslots that end on or before the specified date. "
"By default, this is set to value of the start filter + 60 days."
),
)
def filter_surrounding(self, queryset: QuerySet, name: str, value: datetime.datetime):
nearest_timeslots_in_future = (
models.TimeSlot.objects.filter(start__gte=value)
.order_by("start")
.values_list("id", flat=True)[:5]
)
nearest_timeslots_in_past = (
models.TimeSlot.objects.filter(start__lt=value)
.order_by("-start")
.values_list("id", flat=True)[:5]
)
relevant_timeslot_ids = list(nearest_timeslots_in_future) + list(nearest_timeslots_in_past)
return queryset.filter(id__in=relevant_timeslot_ids)
def filter_start(self, queryset: QuerySet, name: str, value: datetime.date):
start = timezone.make_aware(datetime.datetime.combine(value, datetime.time.min))
return queryset.filter(start__gte=start)
def filter_end(self, queryset: QuerySet, name: str, value: datetime.date):
end = timezone.make_aware(datetime.datetime.combine(value, datetime.time.max))
return queryset.filter(end__lte=end)
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# This is for backwards compatibility as the surrounding-filter was formerly implemented
# by just checking for the existence of the query parameter.
if self.request.GET.get("surrounding", None) == "":
queryset = self.filter_surrounding(queryset, "surrounding", timezone.now())
return queryset
def get_form_class(self):
form_cls = super().get_form_class()
class TimeSlotFilterSetFormWithDefaults(form_cls):
def clean_start(self):
start = self.cleaned_data.get("start", None)
return start or timezone.now().date()
def clean_end(self):
end = self.cleaned_data.get("end", None)
return end or self.cleaned_data["start"] + datetime.timedelta(days=60)
# We only want defaults to apply in the context of the list action.
# When accessing individual timeslots we don’t want the queryset to be restricted
# to the default range of 60 days as get_object would yield a 404 otherwise.
if self.request.parser_context["view"].action == "list":
return TimeSlotFilterSetFormWithDefaults
else:
return form_cls
class Meta:
model = models.TimeSlot
fields = [
"order",
"start",
"end",
"surrounding",
]
class NoteFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only notes matching the specified id(s).",
)
show_owner = IntegerInFilter(
field_name="show__owners",
help_text="Return only notes by show the specified owner(s): all notes the user may edit.",
)
class Meta:
model = models.Note
help_texts = {
"owner": "Return only notes created by the specified user.",
}
fields = ["ids", "owner", "show_owner"]
class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
active = filters.BooleanFilter(field_name="is_active")
class Meta:
fields = [
"active",
]