import datetime from django_filters import rest_framework as filters from django_filters import widgets from django import forms 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", ] 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 class Meta: model = models.TimeSlot fields = [ "order", "start", "end", "surrounding", ] class form(forms.Form): 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)