Skip to content
Snippets Groups Projects
filters.py 10.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • from django_filters import rest_framework as filters
    from django_filters import widgets
    
    
    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)
    
    
        def __init__(self, *args, **kwargs):
    
            kwargs.setdefault("widget", self.QueryArrayWidget())
    
            super().__init__(*args, **kwargs)
    
    
    class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
    
        categoryIds = IntegerInFilter(
            help_text="Return only shows of the given category or categories.",
        )
        categorySlug = filters.CharFilter(
            field_name="category", help_text="Return only shows of the given category slug."
        )
        hostIds = IntegerInFilter(
            field_name="hosts",
            help_text="Return only shows assigned to the given host(s).",
        )
        isActive = 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."
    
        isPublic = filters.BooleanFilter(
            field_name="is_public",
            help_text="Return only shows that are public/non-public.",
        )
        languageIds = IntegerInFilter(
            help_text="Return only shows of the given language(s).",
    
        musicFocusIds = IntegerInFilter(
    
            field_name="music_focus",
            help_text="Return only shows with given music focus(es).",
        )
    
        musicFocusSlug = filters.CharFilter(
    
            field_name="music_focus", help_text="Return only shows with the give music focus slug."
        )
    
        ownerIds = IntegerInFilter(
    
            field_name="owners",
            help_text="Return only shows that belong to the given owner(s).",
        )
    
        topicIds = IntegerInFilter(
    
            help_text="Return only shows of the given topic(s).",
        )
    
        topicSlug = filters.CharFilter(
    
            field_name="topic", help_text="Return only shows of the given topic slug."
        )
    
        typeId = IntegerInFilter(
    
            help_text="Return only shows of a given type.",
        )
    
        typeSlug = filters.CharFilter(
    
            field_name="type", help_text="Return only shows of the given type slug."
        )
    
    
        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
    
                        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)
    
    
            # 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]):
    
        class Meta:
            model = models.Show
            fields = [
    
                "categoryIds",
                "categorySlug",
                "hostIds",
                "isActive",
                "isPublic",
                "languageIds",
                "musicFocusIds",
                "musicFocusSlug",
                "ownerIds",
                "topicIds",
                "topicSlug",
                "typeId",
                "typeSlug",
    
    
    
    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."
            ),
        )
    
    
        scheduleIds = IntegerInFilter(
            field_name="schedule",
            help_text="Return only timeslots that belong to the specified schedule(s).",
        )
    
        showIds = IntegerInFilter(
            field_name="schedule__show",
            help_text="Return only timeslots that belong to the specified show(s).",
        )
    
    
        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):
    
            field_name="id",
            help_text="Return only notes matching the specified id(s).",
        )
    
        ownerIds = IntegerInFilter(
            field_name="owner",
            help_text="Return only notes that belong to the specified owner(s).",
        )
        showIds = IntegerInFilter(
            field_name="timeslot__show",
            help_text="Return only notes that belong to the specified show(s).",
        )
        showOwnerIds = IntegerInFilter(
    
            field_name="timeslot__show__owners",
    
            help_text="Return only notes by show the specified owner(s): all notes the user may edit.",
        )
    
        timeslotIds = IntegerInFilter(
            field_name="timeslot",
            help_text="Return only notes that belong to the specified timeslot(s).",
        )
    
    
        class Meta:
            model = models.Note
            help_texts = {
    
                "ownerId": "Return only notes created by the specified user.",
    
            fields = [
                "ids",
                "ownerIds",
                "showIds",
                "showOwnerIds",
                "timeslotIds",
            ]
    
    class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
    
        isActive = filters.BooleanFilter(field_name="is_active")