Skip to content

Commit 732a52a

Browse files
annashamraySonnyBA
authored andcommitted
Merge pull request #488 from maykinmedia/feature/472-data-attr-contains-comma
support commas in data_attrs query param
2 parents e674625 + 030b012 commit 732a52a

File tree

7 files changed

+676
-74
lines changed

7 files changed

+676
-74
lines changed

.github/workflows/oas-check.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@ on:
1212
jobs:
1313
open-api-workflow-check-oas:
1414
uses: maykinmedia/open-api-workflows/.github/workflows/oas-check.yml@v1
15-
strategy:
16-
matrix:
17-
version:
18-
- v2
1915
with:
20-
schema-path: 'src/objects/api/${{ matrix.version }}/openapi.yaml'
21-
schema-options: "--api-version ${{ matrix.version }}"
16+
schema-path: 'src/objects/api/v2/openapi.yaml'
2217
python-version: '3.11'
2318
django-settings-module: 'objects.conf.ci'
2419
apt-packages: 'libgdal-dev gdal-bin'

src/objects/api/v2/filters.py

+99-45
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,99 @@
11
from datetime import date as date_
22

33
from django import forms
4+
from django.db.models import QuerySet
45
from django.utils.translation import gettext_lazy as _
56

67
from django_filters import filters
78
from rest_framework import serializers
89
from vng_api_common.filtersets import FilterSet
910

1011
from objects.core.models import ObjectRecord, ObjectType
11-
from objects.utils.filters import ObjectTypeFilter
12+
from objects.utils.filters import ManyCharFilter, ObjectTypeFilter
1213

1314
from ..constants import Operators
1415
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
1697

1798

1899
class ObjectRecordFilterForm(forms.Form):
@@ -58,67 +139,40 @@ class ObjectRecordFilterSet(FilterSet):
58139
"date would be between `registrationAt` attributes of different records"
59140
),
60141
)
142+
61143
data_attrs = filters.CharFilter(
62144
method="filter_data_attrs",
63145
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+
)
76148

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,
83153
)
154+
84155
data_icontains = filters.CharFilter(
85156
method="filter_data_icontains",
86157
help_text=_("Search in all `data` values of string properties."),
87158
)
88159

89160
class Meta:
90161
model = ObjectRecord
91-
fields = ("type", "data_attrs", "date", "registrationDate")
162+
fields = ("type", "data_attrs", "data_attr", "date", "registrationDate")
92163
form = ObjectRecordFilterForm
93164

94165
def filter_data_attrs(self, queryset, name, value: str):
95166
parts = value.split(",")
96167

97168
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)
122176

123177
return queryset
124178

src/objects/api/v2/openapi.yaml

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.3
22
info:
33
title: Objects API
4-
version: 2.4.3 (v2)
4+
version: 2.4.3
55
description: |
66
An API to manage Objects.
77
@@ -88,13 +88,52 @@ paths:
8888
description: 'The desired ''Coordinate Reference System'' (CRS) of the response
8989
data. According to the GeoJSON spec, WGS84 is the default (EPSG: 4326 is
9090
the same as WGS84).'
91+
- in: query
92+
name: data_attr
93+
schema:
94+
type: string
95+
description: |
96+
Only include objects that have attributes with certain values.
97+
98+
A valid parameter value has the form `key__operator__value`.
99+
`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value.
100+
Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD).
101+
102+
Valid operator values are:
103+
* `exact` - equal to
104+
* `gt` - greater than
105+
* `gte` - greater than or equal to
106+
* `lt` - lower than
107+
* `lte` - lower than or equal to
108+
* `icontains` - case-insensitive partial match
109+
* `in` - in a list of values separated by `|`
110+
111+
`value` may not contain double underscore or comma characters.
112+
`key` may not contain comma characters and includes double underscore only if it indicates nested attributes.
113+
114+
115+
116+
Example: in order to display only objects with `height` equal to 100, query `data_attr=height__exact__100`
117+
should be used. If `height` is nested inside `dimensions` attribute, query should look like
118+
`data_attr=dimensions__height__exact__100`
119+
120+
This filter is very similar to the old `data_attrs` filter, but it has two differences:
121+
122+
* `value` may contain commas
123+
* only one filtering expression is allowed
124+
125+
If you want to use several filtering expressions, just use this `data_attr` several times in the query string.
126+
Example: `data_attr=height__exact__100&data_attr=naam__icontains__boom`
127+
explode: true
91128
- in: query
92129
name: data_attrs
93130
schema:
94131
type: string
95132
description: |
133+
**DEPRECATED: Use 'data_attr' instead**.
96134
Only include objects that have attributes with certain values.
97135
Data filtering expressions are comma-separated and are structured as follows:
136+
98137
A valid parameter value has the form `key__operator__value`.
99138
`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value.
100139
Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD).
@@ -111,9 +150,15 @@ paths:
111150
`value` may not contain double underscore or comma characters.
112151
`key` may not contain comma characters and includes double underscore only if it indicates nested attributes.
113152
153+
154+
114155
Example: in order to display only objects with `height` equal to 100, query `data_attrs=height__exact__100`
115156
should be used. If `height` is nested inside `dimensions` attribute, query should look like
116157
`data_attrs=dimensions__height__exact__100`
158+
159+
`value` may not contain comma, since commas are used as separator between filtering expressions.
160+
If you want to use commas in `value` you can use `data_attr` query parameter.
161+
deprecated: true
117162
- in: query
118163
name: data_icontains
119164
schema:
@@ -620,8 +665,10 @@ paths:
620665
data_attrs:
621666
type: string
622667
description: |
668+
**DEPRECATED: Use 'data_attr' instead**.
623669
Only include objects that have attributes with certain values.
624670
Data filtering expressions are comma-separated and are structured as follows:
671+
625672
A valid parameter value has the form `key__operator__value`.
626673
`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value.
627674
Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD).
@@ -638,9 +685,48 @@ paths:
638685
`value` may not contain double underscore or comma characters.
639686
`key` may not contain comma characters and includes double underscore only if it indicates nested attributes.
640687
688+
689+
641690
Example: in order to display only objects with `height` equal to 100, query `data_attrs=height__exact__100`
642691
should be used. If `height` is nested inside `dimensions` attribute, query should look like
643692
`data_attrs=dimensions__height__exact__100`
693+
694+
`value` may not contain comma, since commas are used as separator between filtering expressions.
695+
If you want to use commas in `value` you can use `data_attr` query parameter.
696+
data_attr:
697+
type: string
698+
description: |
699+
Only include objects that have attributes with certain values.
700+
701+
A valid parameter value has the form `key__operator__value`.
702+
`key` is the attribute name, `operator` is the comparison operator to be used and `value` is the attribute value.
703+
Note: Values can be string, numeric, or dates (ISO format; YYYY-MM-DD).
704+
705+
Valid operator values are:
706+
* `exact` - equal to
707+
* `gt` - greater than
708+
* `gte` - greater than or equal to
709+
* `lt` - lower than
710+
* `lte` - lower than or equal to
711+
* `icontains` - case-insensitive partial match
712+
* `in` - in a list of values separated by `|`
713+
714+
`value` may not contain double underscore or comma characters.
715+
`key` may not contain comma characters and includes double underscore only if it indicates nested attributes.
716+
717+
718+
719+
Example: in order to display only objects with `height` equal to 100, query `data_attr=height__exact__100`
720+
should be used. If `height` is nested inside `dimensions` attribute, query should look like
721+
`data_attr=dimensions__height__exact__100`
722+
723+
This filter is very similar to the old `data_attrs` filter, but it has two differences:
724+
725+
* `value` may contain commas
726+
* only one filtering expression is allowed
727+
728+
If you want to use several filtering expressions, just use this `data_attr` several times in the query string.
729+
Example: `data_attr=height__exact__100&data_attr=naam__icontains__boom`
644730
date:
645731
type: string
646732
format: date

src/objects/api/v2/views.py

+27-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
from django.db import models
55
from django.utils.dateparse import parse_date
66

7-
from drf_spectacular.types import OpenApiTypes
8-
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
7+
from drf_spectacular.utils import (
8+
OpenApiParameter,
9+
OpenApiTypes,
10+
extend_schema,
11+
extend_schema_view,
12+
)
913
from rest_framework import mixins, viewsets
1014
from rest_framework.decorators import action
1115
from rest_framework.generics import get_object_or_404
@@ -28,13 +32,32 @@
2832
PermissionSerializer,
2933
)
3034
from ..utils import is_date
31-
from .filters import ObjectRecordFilterSet
35+
from .filters import DATA_ATTR_HELP_TEXT, DATA_ATTRS_HELP_TEXT, ObjectRecordFilterSet
36+
37+
# manually override OAS because of "deprecated" attribute
38+
data_attrs_parameter = OpenApiParameter(
39+
name="data_attrs",
40+
type=OpenApiTypes.STR,
41+
location=OpenApiParameter.QUERY,
42+
description=DATA_ATTRS_HELP_TEXT,
43+
deprecated=True,
44+
)
45+
46+
# manually override OAS because of "explode" attribute
47+
data_attr_parameter = OpenApiParameter(
48+
name="data_attr",
49+
location=OpenApiParameter.QUERY,
50+
type=OpenApiTypes.STR,
51+
description=DATA_ATTR_HELP_TEXT,
52+
explode=True,
53+
)
3254

3355

3456
@extend_schema_view(
3557
list=extend_schema(
3658
description="Retrieve a list of OBJECTs and their actual RECORD. "
37-
"The actual record is defined as if the query parameter `date=<today>` was given."
59+
"The actual record is defined as if the query parameter `date=<today>` was given.",
60+
parameters=[data_attrs_parameter, data_attr_parameter],
3861
),
3962
retrieve=extend_schema(
4063
description="Retrieve a single OBJECT and its actual RECORD. "

0 commit comments

Comments
 (0)