|
1 | 1 | from datetime import date as date_
|
2 | 2 |
|
3 | 3 | from django import forms
|
| 4 | +from django.db.models import QuerySet |
4 | 5 | from django.utils.translation import gettext_lazy as _
|
5 | 6 |
|
6 | 7 | from django_filters import filters
|
7 | 8 | from rest_framework import serializers
|
8 | 9 | from vng_api_common.filtersets import FilterSet
|
9 | 10 |
|
10 | 11 | from objects.core.models import ObjectRecord, ObjectType
|
11 |
| -from objects.utils.filters import ObjectTypeFilter |
| 12 | +from objects.utils.filters import ManyCharFilter, ObjectTypeFilter |
12 | 13 |
|
13 | 14 | from ..constants import Operators
|
14 | 15 | from ..utils import display_choice_values_for_help_text, string_to_value
|
15 |
| -from ..validators import validate_data_attrs |
| 16 | +from ..validators import validate_data_attr, validate_data_attrs |
| 17 | + |
| 18 | +DATA_ATTR_VALUE_HELP_TEXT = f"""A valid parameter value has the form `key__operator__value`. |
| 19 | +`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value. |
| 20 | +Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD). |
| 21 | +
|
| 22 | +Valid operator values are: |
| 23 | +{display_choice_values_for_help_text(Operators)} |
| 24 | +
|
| 25 | +`value` may not contain double underscore or comma characters. |
| 26 | +`key` may not contain comma characters and includes double underscore only if it indicates nested attributes. |
| 27 | +
|
| 28 | +""" |
| 29 | + |
| 30 | +DATA_ATTRS_HELP_TEXT = ( |
| 31 | + _( |
| 32 | + """**DEPRECATED: Use 'data_attr' instead**. |
| 33 | +Only include objects that have attributes with certain values. |
| 34 | +Data filtering expressions are comma-separated and are structured as follows: |
| 35 | +
|
| 36 | +%(value_part_help_text)s |
| 37 | +
|
| 38 | +Example: in order to display only objects with `height` equal to 100, query `data_attrs=height__exact__100` |
| 39 | +should be used. If `height` is nested inside `dimensions` attribute, query should look like |
| 40 | +`data_attrs=dimensions__height__exact__100` |
| 41 | +
|
| 42 | +`value` may not contain comma, since commas are used as separator between filtering expressions. |
| 43 | +If you want to use commas in `value` you can use `data_attr` query parameter. |
| 44 | +""" |
| 45 | + ) |
| 46 | + % {"value_part_help_text": DATA_ATTR_VALUE_HELP_TEXT} |
| 47 | +) |
| 48 | + |
| 49 | +DATA_ATTR_HELP_TEXT = ( |
| 50 | + _( |
| 51 | + """Only include objects that have attributes with certain values. |
| 52 | +
|
| 53 | +%(value_part_help_text)s |
| 54 | +
|
| 55 | +Example: in order to display only objects with `height` equal to 100, query `data_attr=height__exact__100` |
| 56 | +should be used. If `height` is nested inside `dimensions` attribute, query should look like |
| 57 | +`data_attr=dimensions__height__exact__100` |
| 58 | +
|
| 59 | +This filter is very similar to the old `data_attrs` filter, but it has two differences: |
| 60 | +
|
| 61 | +* `value` may contain commas |
| 62 | +* only one filtering expression is allowed |
| 63 | +
|
| 64 | +If you want to use several filtering expressions, just use this `data_attr` several times in the query string. |
| 65 | +Example: `data_attr=height__exact__100&data_attr=naam__icontains__boom` |
| 66 | +""" |
| 67 | + ) |
| 68 | + % {"value_part_help_text": DATA_ATTR_VALUE_HELP_TEXT} |
| 69 | +) |
| 70 | + |
| 71 | + |
| 72 | +def filter_data_attr_value_part(value_part: str, queryset: QuerySet) -> QuerySet: |
| 73 | + """ |
| 74 | + filter one value part for data_attr and data_attrs filters |
| 75 | + """ |
| 76 | + variable, operator, str_value = value_part.rsplit("__", 2) |
| 77 | + real_value = string_to_value(str_value) |
| 78 | + |
| 79 | + if operator == "exact": |
| 80 | + # for exact operator try to filter on string and numeric values |
| 81 | + in_vals = [str_value] |
| 82 | + if real_value != str_value: |
| 83 | + in_vals.append(real_value) |
| 84 | + queryset = queryset.filter(**{f"data__{variable}__in": in_vals}) |
| 85 | + elif operator == "icontains": |
| 86 | + # icontains treats everything like strings |
| 87 | + queryset = queryset.filter(**{f"data__{variable}__icontains": str_value}) |
| 88 | + elif operator == "in": |
| 89 | + # in must be a list |
| 90 | + values = str_value.split("|") |
| 91 | + queryset = queryset.filter(**{f"data__{variable}__in": values}) |
| 92 | + |
| 93 | + else: |
| 94 | + # gt, gte, lt, lte operators |
| 95 | + queryset = queryset.filter(**{f"data__{variable}__{operator}": real_value}) |
| 96 | + return queryset |
16 | 97 |
|
17 | 98 |
|
18 | 99 | class ObjectRecordFilterForm(forms.Form):
|
@@ -58,67 +139,40 @@ class ObjectRecordFilterSet(FilterSet):
|
58 | 139 | "date would be between `registrationAt` attributes of different records"
|
59 | 140 | ),
|
60 | 141 | )
|
| 142 | + |
61 | 143 | data_attrs = filters.CharFilter(
|
62 | 144 | method="filter_data_attrs",
|
63 | 145 | validators=[validate_data_attrs],
|
64 |
| - help_text=_( |
65 |
| - """Only include objects that have attributes with certain values. |
66 |
| -Data filtering expressions are comma-separated and are structured as follows: |
67 |
| -A valid parameter value has the form `key__operator__value`. |
68 |
| -`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value. |
69 |
| -Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD). |
70 |
| -
|
71 |
| -Valid operator values are: |
72 |
| -%(operator_choices)s |
73 |
| -
|
74 |
| -`value` may not contain double underscore or comma characters. |
75 |
| -`key` may not contain comma characters and includes double underscore only if it indicates nested attributes. |
| 146 | + help_text=DATA_ATTRS_HELP_TEXT, |
| 147 | + ) |
76 | 148 |
|
77 |
| -Example: in order to display only objects with `height` equal to 100, query `data_attrs=height__exact__100` |
78 |
| -should be used. If `height` is nested inside `dimensions` attribute, query should look like |
79 |
| -`data_attrs=dimensions__height__exact__100` |
80 |
| -""" |
81 |
| - ) |
82 |
| - % {"operator_choices": display_choice_values_for_help_text(Operators)}, |
| 149 | + data_attr = ManyCharFilter( |
| 150 | + method="filter_data_attr", |
| 151 | + validators=[validate_data_attr], |
| 152 | + help_text=DATA_ATTR_HELP_TEXT, |
83 | 153 | )
|
| 154 | + |
84 | 155 | data_icontains = filters.CharFilter(
|
85 | 156 | method="filter_data_icontains",
|
86 | 157 | help_text=_("Search in all `data` values of string properties."),
|
87 | 158 | )
|
88 | 159 |
|
89 | 160 | class Meta:
|
90 | 161 | model = ObjectRecord
|
91 |
| - fields = ("type", "data_attrs", "date", "registrationDate") |
| 162 | + fields = ("type", "data_attrs", "data_attr", "date", "registrationDate") |
92 | 163 | form = ObjectRecordFilterForm
|
93 | 164 |
|
94 | 165 | def filter_data_attrs(self, queryset, name, value: str):
|
95 | 166 | parts = value.split(",")
|
96 | 167 |
|
97 | 168 | for value_part in parts:
|
98 |
| - variable, operator, str_value = value_part.rsplit("__", 2) |
99 |
| - real_value = string_to_value(str_value) |
100 |
| - |
101 |
| - if operator == "exact": |
102 |
| - # for exact operator try to filter on string and numeric values |
103 |
| - in_vals = [str_value] |
104 |
| - if real_value != value: |
105 |
| - in_vals.append(real_value) |
106 |
| - queryset = queryset.filter(**{f"data__{variable}__in": in_vals}) |
107 |
| - elif operator == "icontains": |
108 |
| - # icontains treats everything like strings |
109 |
| - queryset = queryset.filter( |
110 |
| - **{f"data__{variable}__icontains": str_value} |
111 |
| - ) |
112 |
| - elif operator == "in": |
113 |
| - # in must be a list |
114 |
| - values = str_value.split("|") |
115 |
| - queryset = queryset.filter(**{f"data__{variable}__in": values}) |
116 |
| - |
117 |
| - else: |
118 |
| - # gt, gte, lt, lte operators |
119 |
| - queryset = queryset.filter( |
120 |
| - **{f"data__{variable}__{operator}": real_value} |
121 |
| - ) |
| 169 | + queryset = filter_data_attr_value_part(value_part, queryset) |
| 170 | + |
| 171 | + return queryset |
| 172 | + |
| 173 | + def filter_data_attr(self, queryset, name, value: list): |
| 174 | + for value_part in value: |
| 175 | + queryset = filter_data_attr_value_part(value_part, queryset) |
122 | 176 |
|
123 | 177 | return queryset
|
124 | 178 |
|
|
0 commit comments