class OrganizationFilter(FilterSet): """ Фильтр категорий товаров и сети для общего списка организаций """ category = filters.ModelMultipleChoiceFilter( queryset=Category.objects.all(), field_name='connector__product__category__name', to_field_name='name', label='Категория товаров/услуг') network = filters.ModelMultipleChoiceFilter(queryset=Network.objects.all(), field_name='network__name', to_field_name='name', label='Сеть организаций') class Meta: model = Organization fields = ['category', 'network']
class SignalCategoryRemovedAfterFilterSet(FilterSet): after = filters.IsoDateTimeFilter(field_name='category_assignment__created_at', lookup_expr='gte') before = filters.IsoDateTimeFilter(field_name='category_assignment__created_at', lookup_expr='lte') category_slug = filters.ModelMultipleChoiceFilter( method='category_filter', queryset=Category.objects.all(), to_field_name='slug', field_name='category_assignment__category__slug', ) def category_filter(self, queryset, name, value): # TODO: Get categories from user permissions, can be added after PR # https://github.com/Amsterdam/signals/pull/202 has been approved and merged if len(value): # We need to check the given categories categories_to_check = [v.id for v in value] else: # A list of category id's that the currently logged in user has permissions for categories_to_check = Category.objects.all().values_list('id', flat=True) return queryset.filter( Q(category_assignment__isnull=False) & Q(categories__id__in=categories_to_check) & ~Q(category_assignment__category_id__in=categories_to_check) )
class UserNameListFilterSet(FilterSet): username = filters.CharFilter(lookup_expr='icontains', min_length=3) is_active = filters.BooleanFilter(field_name='is_active') profile_department_code = filters.ModelMultipleChoiceFilter( queryset=_get_department_queryset(), to_field_name='code', field_name='profile__departments__code')
class SimpleOffersFilter(FilterSet): tags = filters.ModelMultipleChoiceFilter(queryset=Tag.objects.all(), name="tags__name", to_field_name='name', conjoined=True) class Meta: model = Offer fields = ['tags']
def __init__(self, *args, **kwargs): super().__init__() prod_id = kwargs['queryset'][0].organization.pk self.filters['category'] = filters.ModelMultipleChoiceFilter( queryset=Category.objects.filter( product__connector__organization__id=prod_id).distinct(), field_name='product__category__name', to_field_name='name', label='Категория товаров/услуг')
class ProductFilter(FilterSet): """ Фильтр категорий товаров, сети, мин. и макс. цены для общего списка товаров """ category = filters.ModelMultipleChoiceFilter( queryset=Category.objects.all(), field_name='connector__product__category__name', to_field_name='name', label='Категория товаров/услуг') network = filters.ModelMultipleChoiceFilter(queryset=Network.objects.all(), field_name='network__name', to_field_name='name', label='Сеть организаций') min_price = filters.NumberFilter(field_name='price', lookup_expr='gte', label='Минимальная цена') max_price = filters.NumberFilter(field_name='price', lookup_expr='lte', label='Максимальная цена') class Meta: model = Product fields = { 'min_price': 'gte', 'max_price': 'lte', }
class UserFilterSet(FilterSet): id = filters.NumberFilter() username = filters.CharFilter(lookup_expr='icontains') is_active = filters.BooleanFilter(field_name='is_active') role = filters.ModelMultipleChoiceFilter(queryset=_get_group_queryset(), to_field_name='name', field_name='groups__name') profile_department_code = filters.ModelMultipleChoiceFilter( queryset=_get_department_queryset(), to_field_name='code', field_name='profile__departments__code') order = OrderingExtraKwargsFilter( fields=( ('username', 'username'), ('is_active', 'is_active'), ), extra_kwargs={ 'username': { 'apply': Lower } # Will apply the Lower function when ordering })
class FunctionFilter(filterset.FilterSet): """ ## Filters To filter for exact value matches: ?<fieldname>=<value> For advanced filtering use lookups: ?<fieldname>__<lookup>=<value> Possible lookups: - `name`: `iexact`, `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `regex`, `iregex` """ category = filters.ChoiceFilter(label=_("Category"), choices=models.Function.CATEGORY_CHOICES) persons = filters.ModelMultipleChoiceFilter( label=_("Person"), queryset=models.Person.objects.all()) class Meta: model = models.Function fields = { "name": ( "exact", "iexact", "contains", "icontains", "startswith", "istartswith", "endswith", "iendswith", "regex", "iregex", ), "leader": ("exact", ), }
class SignalFilterSet(FilterSet): id = filters.NumberFilter() address_text = filters.CharFilter(field_name='location__address_text', lookup_expr='icontains') area_code = filters.ChoiceFilter(field_name='location__area_code', choices=area_choices) area_type_code = filters.ChoiceFilter(field_name='location__area_type_code', choices=area_type_choices) buurt_code = filters.MultipleChoiceFilter(field_name='location__buurt_code', choices=buurt_choices) category_id = filters.MultipleChoiceFilter(field_name='category_assignment__category_id', choices=category_choices) category_slug = filters.ModelMultipleChoiceFilter( queryset=_get_child_category_queryset(), to_field_name='slug', field_name='category_assignment__category__slug', ) contact_details = filters.MultipleChoiceFilter(method='contact_details_filter', choices=contact_details_choices) created_before = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte') directing_department = filters.MultipleChoiceFilter( method='directing_department_filter', choices=department_choices ) created_after = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte') feedback = filters.ChoiceFilter(method='feedback_filter', choices=feedback_choices) has_changed_children = filters.MultipleChoiceFilter(method='has_changed_children_filter', choices=boolean_choices) kind = filters.MultipleChoiceFilter(method='kind_filter', choices=kind_choices) # SIG-2636 incident_date = filters.DateFilter(field_name='incident_date_start', lookup_expr='date') incident_date_before = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__gte') incident_date_after = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__lte') maincategory_slug = filters.ModelMultipleChoiceFilter( queryset=_get_parent_category_queryset(), to_field_name='slug', field_name='category_assignment__category__parent__slug', ) note_keyword = filters.CharFilter(method='note_keyword_filter') priority = filters.MultipleChoiceFilter(field_name='priority__priority', choices=Priority.PRIORITY_CHOICES) source = filters.MultipleChoiceFilter(choices=source_choices) stadsdeel = filters.MultipleChoiceFilter(field_name='location__stadsdeel', choices=stadsdelen_choices) status = filters.MultipleChoiceFilter(field_name='status__state', choices=status_choices) type = filters.MultipleChoiceFilter(field_name='type_assignment__name', choices=Type.CHOICES) updated_before = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='lte') updated_after = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='gte') def _cleanup_form_data(self): """ Cleanup the form data """ self.form.cleaned_data.pop('category_slug', None) self.form.cleaned_data.pop('maincategory_slug', None) if not self.form.cleaned_data.get('area_code', None) or not self.form.cleaned_data.get('area_type_code', None): self.form.cleaned_data.pop('area_code', None) self.form.cleaned_data.pop('area_type_code', None) def filter_queryset(self, queryset): """ Add custom category filtering to the filter_queryset """ if not self.form.cleaned_data['category_id']: main_categories = self.form.cleaned_data['maincategory_slug'] sub_categories = self.form.cleaned_data['category_slug'] if main_categories or sub_categories: queryset = queryset.filter( Q(category_assignment__category__parent_id__in=[c.pk for c in main_categories]) | Q(category_assignment__category_id__in=[c.pk for c in sub_categories]) ) self._cleanup_form_data() queryset = super(SignalFilterSet, self).filter_queryset(queryset=queryset) return queryset.distinct() # Custom filter functions def contact_details_filter(self, queryset, name, value): """ Filter `signals.Signal` instances according to presence of contact details. """ choices = value # we have a MultipleChoiceFilter ... # Deal with all choices selected, or none selected: if len(choices) == len(contact_details_choices()): # No filters are present, or ... all filters are present. In that # case we want all Signal instances with an email address, or a # phone number, or none of those (according to) the UX design. # This is the same as not filtering, hence in both cases just # return the queryset. return queryset # Set-up our Q objects for the individual options. has_no_email = (Q(reporter__email__isnull=True) | Q(reporter__email='')) has_no_phone = (Q(reporter__phone__isnull=True) | Q(reporter__phone='')) is_anonymous = has_no_email & has_no_phone q_objects = { 'email': ~has_no_email, 'phone': ~has_no_phone, 'none': is_anonymous, } # The individual choices have to be combined using logical OR: q_total = q_objects[choices.pop()] while choices: q_total |= q_objects[choices.pop()] return queryset.filter(q_total) def directing_department_filter(self, queryset, name, value): """ Filter Signals on directing department. * A directing department can only be set on a Parent Signal * When providing the option "null" select all parent Signals without one or more directing departments * When providing on or more department codes as options select all parent Signals which match directing departments Example 1: "?directing_department=ASC" will select all parent Signals where ASC is the directing department Example 2: "?directing_department=ASC&directing_department=null" will select all parent Signals without a directing department OR ASC as the directing department Example 3: "?directing_department=null" will select all parent Signals without a directing department """ choices = value # we have a MultipleChoiceFilter ... if len(choices) == len(department_choices()): # No filters are present, or ... all filters are present. In that case we want all Signal instances return queryset # Directing departments are only set on parent Signals parent_q_filter = (Q(parent__isnull=True) & Q(children__isnull=False)) if 'null' in choices and len(choices) == 1: # "?directing_department=null" will select all parent Signals without a directing department return queryset.filter( parent_q_filter & ( Q(directing_departments_assignment__isnull=True) | Q(directing_departments_assignment__departments__isnull=True) ) ) elif 'null' in choices and len(choices) > 1: # "?directing_department=ASC&directing_department=null" will select all parent Signals without a directing # department OR ASC as the directing department choices.pop(choices.index('null')) return queryset.filter( parent_q_filter & ( Q(directing_departments_assignment__isnull=True) | Q(directing_departments_assignment__departments__isnull=True) | Q(directing_departments_assignment__departments__code__in=choices) ) ) elif len(choices): # "?directing_department=ASC" will select all parent Signals where ASC is the directing department return queryset.filter( parent_q_filter & Q(directing_departments_assignment__departments__code__in=choices) ) return queryset def feedback_filter(self, queryset, name, value): # Only signals that have feedback queryset = queryset.annotate(feedback_count=Count('feedback')).filter(feedback_count__gte=1) if value in ['satisfied', 'not_satisfied']: is_satisfied = True if value == 'satisfied' else False queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at'), feedback_max_submitted_at=Max('feedback__submitted_at') ).filter( feedback__is_satisfied=is_satisfied, feedback__submitted_at__isnull=False, feedback__created_at=F('feedback_max_created_at'), feedback__submitted_at=F('feedback_max_submitted_at') ) elif value == 'not_received': queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at') ).filter( feedback__submitted_at__isnull=True, feedback__created_at=F('feedback_max_created_at') ) return queryset def kind_filter(self, queryset, name, value): choices = value # we have a MultipleChoiceFilter ... if (len(choices) == len(kind_choices()) or {'signal', 'parent_signal', 'child_signal'} == set(choices) or {'parent_signal', 'exclude_parent_signal'} == set(choices)): return queryset q_objects = { 'signal': (Q(parent__isnull=True) & Q(children__isnull=True)), 'parent_signal': (Q(parent__isnull=True) & Q(children__isnull=False)), 'exclude_parent_signal': ~(Q(parent__isnull=True) & Q(children__isnull=False)), 'child_signal': (Q(parent__isnull=False)), } q_filter = q_objects[choices.pop()] while choices: q_filter |= q_objects[choices.pop()] return queryset.filter(q_filter) if q_filter else queryset def note_keyword_filter(self, queryset, name, value): return queryset.filter(notes__text__icontains=value) def has_changed_children_filter(self, queryset, name, value): # we have a MultipleChoiceFilter ... choices = list(set(map(lambda x: True if x in [True, 'True', 'true', 1] else False, value))) q_filter = Q(children__isnull=False) if len(choices) == 2: return queryset.filter(q_filter) if True in choices: q_filter &= Q(updated_at__lt=F('children__updated_at')) # noqa Selects all parent signals with changes in the child signals if False in choices: q_filter &= Q(updated_at__gt=F('children__updated_at')) # noqa Selects all parent signals with NO changes in the child signals return queryset.filter(q_filter)
class SignalFilter(FilterSet): """ !!! This is the filter used in the V0 version of the API. V0 will be deprecated soon !!! - V1 filters can be found in signals/apps/api/v1/filters.py """ id = IntegerFilter() in_bbox = filters.CharFilter(method='in_bbox_filter', label='bbox') geo = filters.CharFilter(method="locatie_filter", label='x,y,r') location__stadsdeel = filters.MultipleChoiceFilter(choices=STADSDELEN) location__buurt_code = filters.MultipleChoiceFilter(choices=buurt_choices) location__address_text = filters.CharFilter(lookup_expr='icontains') created_at = filters.DateFilter(field_name='created_at', lookup_expr='date') created_at__gte = filters.DateFilter(field_name='created_at', lookup_expr='date__gte') created_at__lte = filters.DateFilter(field_name='created_at', lookup_expr='date__lte') updated_at = filters.DateFilter(field_name='updated_at', lookup_expr='date') updated_at__gte = filters.DateFilter(field_name='updated_at', lookup_expr='date__gte') updated_at__lte = filters.DateFilter(field_name='updated_at', lookup_expr='date__lte') incident_date_start = filters.DateFilter(field_name='incident_date_start', lookup_expr='date') incident_date_start__gte = filters.DateFilter( field_name='incident_date_start', lookup_expr='date__gte') incident_date_start__lte = filters.DateFilter( field_name='incident_date_start', lookup_expr='date__lte') incident_date_end = filters.DateFilter(field_name='incident_date_end', lookup_expr='date') operational_date = filters.DateFilter(field_name='operational_date', lookup_expr='date') expire_date = filters.DateFilter(field_name='expire_date', lookup_expr='date') expire_date__gte = filters.DateFilter(field_name='expire_date', lookup_expr='date__gte') expire_date__lte = filters.DateFilter(field_name='expire_date', lookup_expr='date__lte') status__state = filters.MultipleChoiceFilter(choices=status_choices) # TODO: these filter (category__main, category__sub) should be removed category__main = filters.ModelMultipleChoiceFilter( queryset=Category.objects.filter(parent__isnull=True), to_field_name='name', field_name='category_assignment__category__parent__name') category__sub = filters.ModelMultipleChoiceFilter( queryset=Category.objects.all(), to_field_name='name', field_name='category_assignment__category__name') # category__main and category__sub filters will be replaced with main_slug and sub_slug main_slug = filters.ModelMultipleChoiceFilter( queryset=Category.objects.filter(parent__isnull=True).select_related(), to_field_name='slug', field_name='category_assignment__category__parent__slug', ) sub_slug = filters.ModelMultipleChoiceFilter( queryset=Category.objects.all().select_related(), to_field_name='slug', field_name='category_assignment__category__slug', ) priority__priority = filters.MultipleChoiceFilter( choices=Priority.PRIORITY_CHOICES) class Meta(object): model = Signal fields = ( 'id', 'signal_id', 'status__state', 'category__main', 'category__sub', 'main_slug', 'sub_slug', 'location__buurt_code', 'location__stadsdeel', 'location__address_text', 'reporter__email', 'in_bbox', 'geo', ) def in_bbox_filter(self, qs, name, value): bbox_values, err = bbox.valid_bbox(value) lon1, lat1, lon2, lat2 = bbox_values poly_bbox = Polygon.from_bbox((lon1, lat1, lon2, lat2)) if err: raise ValidationError(f"bbox invalid {err}:{bbox_values}") return qs.filter(location__geometrie__bboverlaps=poly_bbox) def locatie_filter(self, qs, name, value): lon, lat, radius = parse_xyr(value) point = Point(lon, lat) return qs.filter( location__geometrie__dwithin=(point, bbox.dist_to_deg(radius, lat)))
class PersonFilter(filterset.FilterSet): """ ## Filters To filter for exact value matches: ?<fieldname>=<value> For advanced filtering use lookups: ?<fieldname>__<lookup>=<value> Possible lookups: - `first_name`: `iexact`, `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `isnull`, `regex`, `iregex` - `last_name`: `iexact`, `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `isnull`, `regex`, `iregex` - `title`: `iexact`, `contains`, `icontains`, `isnull`, `regex`, `iregex` - `consultation`: `contains`, `icontains`, `isnull`, `regex`, `iregex` - `appendix`: `contains`, `icontains`, `isnull`, `regex`, `iregex` """ sex = filters.ChoiceFilter(label=_("Sex"), choices=models.Person.GENDER_CHOICES) functions = filters.ModelMultipleChoiceFilter( label=_("Function"), queryset=models.Function.objects.all()) class Meta: model = models.Person fields = { "first_name": ( "exact", "iexact", "contains", "icontains", "startswith", "istartswith", "endswith", "iendswith", "isnull", "regex", "iregex", ), "last_name": ( "exact", "iexact", "contains", "icontains", "startswith", "istartswith", "endswith", "iendswith", "isnull", "regex", "iregex", ), "title": ( "exact", "iexact", "contains", "icontains", "isnull", "regex", "iregex", ), "consultation": ("contains", "icontains", "isnull", "regex", "iregex"), "appendix": ("contains", "icontains", "isnull", "regex", "iregex"), }
class AlertFilter(FilterSet): """ Filters Alerts. """ def __init__(self, *args, **kwargs): super(AlertFilter, self).__init__(*args, **kwargs) # add a blank choice to ChoiceFilter options for (dummy_name, field) in self.filters.items(): if isinstance(field, django_filters.ChoiceFilter): field.extra['choices'] = tuple([('', '---------'), ] + \ list(field.extra['choices'])) collection = django_filters.ModelMultipleChoiceFilter( name='distillery', label='Collections', queryset=Distillery.objects.have_alerts()) warehouse = django_filters.ModelMultipleChoiceFilter( name='distillery__collection__warehouse', label='Warehouses', queryset=Warehouse.objects.all()) after = django_filters.DateTimeFilter(name='created_date', lookup_expr='gt') before = django_filters.DateTimeFilter(name='created_date', lookup_expr='lte') level = django_filters.MultipleChoiceFilter(choices=ALERT_LEVEL_CHOICES) status = django_filters.MultipleChoiceFilter(choices=ALERT_STATUS_CHOICES) assigned_user = django_filters.ModelChoiceFilter( name='assigned_user', queryset=AppUser.objects.all()) content = django_filters.CharFilter(name='data', label='Content', method='filter_by_content') categories = django_filters.ModelMultipleChoiceFilter( name='distillery__categories', label='Collection categories', queryset=Category.objects.all()) tags = django_filters.ModelMultipleChoiceFilter(name='tags', queryset=Tag.objects.all()) class Meta: model = Alert # List content field last so it will have fewer Alerts to # filter. The content filter requires a query to the Distillery # associated with the Alert. It's best to filter out as many # records as possible before constructing that query. fields = [ 'collection', 'after', 'before', 'level', 'status', 'assigned_user', 'content' ] @staticmethod def _get_data_query(distillery, value): """ """ text_fields = distillery.get_text_fields() field_names = [field.field_name for field in text_fields] field_keys = [name.replace('.', '__') for name in field_names] queries = [] for key in field_keys: query_exp = 'data__%s' % key kwarg = {query_exp: value} queries.append(Q(**kwarg)) field_query = join_query(queries, 'OR') return Q(distillery=distillery) & field_query @staticmethod def _get_title_query(value): """ """ return Q(title__icontains=value) def _filter_by_value(self, queryset, value): """ """ distilleries = Distillery.objects.filter( alerts__in=queryset).distinct() if distilleries: queries = [self._get_data_query(distillery, value) \ for distillery in distilleries] data_query = join_query(queries, 'OR') title_query = self._get_title_query(value) return queryset.filter(title_query | data_query) else: return queryset.none() # @timeit def filter_by_content(self, queryset, name, value): """ Takes a QuerySet of Alerts and a string value. Returns a filtered QuerySet of Alerts whose data includes the given value. """ if not value: return queryset try: return self._filter_by_value(queryset, value) except ValueError: LOGGER.error('An error occurred while filtering Alerts') return queryset
class SignalFilter(FilterSet): id = filters.NumberFilter() created_before = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte') created_after = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte') updated_before = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='lte') updated_after = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='gte') status = filters.MultipleChoiceFilter(field_name='status__state', choices=status_choices) maincategory_slug = filters.ModelMultipleChoiceFilter( queryset=_get_parent_category_queryset(), to_field_name='slug', field_name='category_assignment__category__parent__slug', ) # category_slug, because we will soon be using one type of category, instead of main vs sub # categories. This way the naming in the API will remain consistent category_slug = filters.ModelMultipleChoiceFilter( queryset=_get_child_category_queryset(), to_field_name='slug', field_name='category_assignment__category__slug', ) priority = filters.ChoiceFilter(field_name='priority__priority', choices=Priority.PRIORITY_CHOICES) stadsdeel = filters.MultipleChoiceFilter(field_name='location__stadsdeel', choices=STADSDELEN) buurt_code = filters.MultipleChoiceFilter(field_name='location__buurt_code', choices=buurt_choices) address_text = filters.CharFilter(field_name='location__address_text', lookup_expr='icontains') incident_date = filters.DateFilter(field_name='incident_date_start', lookup_expr='date') incident_date_before = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__gte') incident_date_after = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__lte') source = filters.MultipleChoiceFilter(choices=source_choices) feedback = filters.ChoiceFilter(method='feedback_filter', choices=feedback_choices) def feedback_filter(self, queryset, name, value): # Only signals that have feedback queryset = queryset.annotate(feedback_count=Count('feedback')).filter(feedback_count__gte=1) if value in ['satisfied', 'not_satisfied']: is_satisfied = True if value == 'satisfied' else False queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at'), feedback_max_submitted_at=Max('feedback__submitted_at') ).filter( feedback__is_satisfied=is_satisfied, feedback__submitted_at__isnull=False, feedback__created_at=F('feedback_max_created_at'), feedback__submitted_at=F('feedback_max_submitted_at') ) elif value == 'not_received': queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at') ).filter( feedback__submitted_at__isnull=True, feedback__created_at=F('feedback_max_created_at') ) return queryset def _categories_filter(self, queryset, main_categories, sub_categories): if not main_categories and not sub_categories: return queryset queryset = queryset.filter( Q(category_assignment__category__parent_id__in=[c.pk for c in main_categories]) | Q(category_assignment__category_id__in=[c.pk for c in sub_categories]) ) return queryset def filter_queryset(self, queryset): main_categories = [] sub_categories = [] for name, value in self.form.cleaned_data.items(): if name.lower() == 'maincategory_slug': main_categories = value elif name.lower() == 'category_slug': sub_categories = value else: queryset = self.filters[name].filter(queryset, value) return self._categories_filter(queryset=queryset, main_categories=main_categories, sub_categories=sub_categories)
class SignalFilter(FilterSet): id = filters.NumberFilter() country = filters.MultipleChoiceFilter(field_name='country') city = filters.MultipleChoiceFilter(field_name='city') created_before = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='lte') created_after = filters.IsoDateTimeFilter(field_name='created_at', lookup_expr='gte') updated_before = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='lte') updated_after = filters.IsoDateTimeFilter(field_name='updated_at', lookup_expr='gte') status = filters.MultipleChoiceFilter(field_name='status__state', choices=status_choices) maincategory_slug = filters.ModelMultipleChoiceFilter( queryset=_get_parent_category_queryset(), to_field_name='slug', field_name='category_assignment__category__parent__slug', ) # category_slug, because we will soon be using one type of category, instead of main vs sub # categories. This way the naming in the API will remain consistent category_slug = filters.ModelMultipleChoiceFilter( queryset=_get_child_category_queryset(), to_field_name='slug', field_name='category_assignment__category__slug', ) category_filter_label = filters.ModelMultipleChoiceFilter( queryset=_get_parent_category_queryset(), to_field_name='filter_label', field_name='category_assignment__category__filter_label', ) priority = filters.MultipleChoiceFilter(field_name='priority__priority', choices=Priority.PRIORITY_CHOICES) stadsdeel = filters.MultipleChoiceFilter(field_name='location__stadsdeel', choices=stadsdelen) buurt_code = filters.MultipleChoiceFilter( field_name='location__buurt_code', choices=buurt_choices) address_text = filters.CharFilter(field_name='location__address_text', lookup_expr='icontains') incident_date = filters.DateFilter(field_name='incident_date_start', lookup_expr='date') incident_date_before = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__gte') incident_date_after = filters.DateFilter(field_name='incident_date_start', lookup_expr='date__lte') source = filters.MultipleChoiceFilter(choices=source_choices) feedback = filters.ChoiceFilter(method='feedback_filter', choices=feedback_choices) contact_details = filters.MultipleChoiceFilter( method='contact_details_filter', choices=contact_details_choices, ) # SIG-2148 Filter on Signal Type type = filters.MultipleChoiceFilter(method='type_filter', choices=Type.CHOICES) note_keyword = filters.CharFilter(method='note_keyword_filter', ) directing_department = filters.MultipleChoiceFilter( field_name='directing_departments_assignment__departments__code', choices=department_choices) def feedback_filter(self, queryset, name, value): # Only signals that have feedback queryset = queryset.annotate(feedback_count=Count('feedback')).filter( feedback_count__gte=1) if value in ['satisfied', 'not_satisfied']: is_satisfied = True if value == 'satisfied' else False queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at'), feedback_max_submitted_at=Max( 'feedback__submitted_at')).filter( feedback__is_satisfied=is_satisfied, feedback__submitted_at__isnull=False, feedback__created_at=F('feedback_max_created_at'), feedback__submitted_at=F('feedback_max_submitted_at')) elif value == 'not_received': queryset = queryset.annotate( feedback_max_created_at=Max('feedback__created_at')).filter( feedback__submitted_at__isnull=True, feedback__created_at=F('feedback_max_created_at')) return queryset def _categories_filter(self, queryset, main_categories, sub_categories): if not main_categories and not sub_categories: return queryset queryset = queryset.filter( Q(category_assignment__category__parent_id__in=[ c.pk for c in main_categories ]) | Q(category_assignment__category_id__in=[ c.pk for c in sub_categories ])) return queryset def filter_queryset(self, queryset): main_categories = [] sub_categories = [] for name, value in self.form.cleaned_data.items(): if name.lower() == 'maincategory_slug': main_categories = value elif name.lower() == 'category_slug': sub_categories = value else: queryset = self.filters[name].filter(queryset, value) return self._categories_filter(queryset=queryset, main_categories=main_categories, sub_categories=sub_categories) def contact_details_filter(self, queryset, name, value): """ Filter `signals.Signal` instances according to presence of contact details. """ # Set-up our Q objects for the individual options. has_no_email = (Q(reporter__email__isnull=True) | Q(reporter__email='')) has_no_phone = (Q(reporter__phone__isnull=True) | Q(reporter__phone='')) is_anonymous = has_no_email & has_no_phone q_objects = { 'email': ~has_no_email, 'phone': ~has_no_phone, 'none': is_anonymous, } choices = value # we have a MultipleChoiceFilter ... # Deal with all choices selected, or none selected: if len(choices) == len(contact_details_choices): # No filters are present, or ... all filters are present. In that # case we want all Signal instances with an email address, or a # phone number, or none of those (according to) the UX design. # This is the same as not filtering, hence in both cases just # return the queryset. return queryset # The individual choices have to be combined using logical OR: q_total = q_objects[choices.pop()] while choices: q_total |= q_objects[choices.pop()] return queryset.filter(q_total) def type_filter(self, queryset, name, value): return queryset.annotate(type_assignment_id=Max('types__id')).filter( types__id=F('type_assignment_id'), types__name__in=value) def note_keyword_filter(self, queryset, name, value): return queryset.filter(notes__text__icontains=value).distinct()
class PublicSignalGeographyFilter(FilterSet): bbox = filters.CharFilter() # min_lon, min_lat, max_lon, max_lat lat = filters.NumberFilter() lon = filters.NumberFilter() maincategory_slug = filters.ModelMultipleChoiceFilter( queryset=_get_parent_category_queryset(), to_field_name='slug', field_name='category_assignment__category__parent__slug', ) # Only parent categories are allowed category_slug = filters.ModelMultipleChoiceFilter( queryset=_get_child_category_queryset().filter( is_public_accessible=True), to_field_name='slug', field_name='category_assignment__category__slug' ) # Only child categories that are public accessible are allowed def is_valid(self): """ Validate if the bbox or lon/lat variable are filled in """ data = self.form.data if data.get('bbox') or (data.get('lon') and data.get('lat')): return self.form.is_valid() raise ValidationError( {"non_field_errors": ["Either bbox or lon/lat must be filled in"]}) def filter_queryset(self, queryset): """ Filters Signal's in a given bbox and category """ bbox = self.form.cleaned_data.pop('bbox', None) bbox = bbox.split(',') if bbox else None main_categories = self.form.cleaned_data.pop('maincategory_slug') sub_categories = self.form.cleaned_data.pop('category_slug') lat = self.form.cleaned_data.pop('lat', None) lon = self.form.cleaned_data.pop('lon', None) geometrie_filter = Q(location__geometrie__within=Polygon.from_bbox(bbox)) if bbox \ else Q(location__geometrie=Point(float(lon), float(lat), srid=4326)) category_filter = Q() if main_categories: category_filter &= Q(category_assignment__category__parent_id__in=[ c.pk for c in main_categories ]) if sub_categories: category_filter &= Q(category_assignment__category_id__in=[ c.pk for c in sub_categories ]) return super().filter_queryset(queryset=queryset.filter( category_filter & # Filter Signal's in the given bounding box geometrie_filter))