Skip to content

Commit 73fd1c8

Browse files
committed
Add Tabular and Vector data filters based on data/metadata JSON fields
1 parent 94d96c2 commit 73fd1c8

File tree

5 files changed

+142
-11
lines changed

5 files changed

+142
-11
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ django_unique_upload==0.2.1
1515
# Rest apis
1616
djangorestframework==3.16.1
1717
djangorestframework-gis==1.2.0
18-
django-filter==24.3
18+
django-filter==25.1
1919
drf_spectacular==0.28.0
2020
django-cors-headers==4.7.0
2121
drf-excel==2.5.3

vbos/datasets/filters.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
from django_filters import (
22
FilterSet,
3-
BooleanFilter,
43
CharFilter,
54
OrderingFilter,
65
DateFromToRangeFilter,
76
ModelChoiceFilter,
87
)
98

10-
from .models import RasterDataset, VectorDataset, TabularDataset, Cluster
9+
from .models import (
10+
RasterDataset,
11+
TabularItem,
12+
VectorDataset,
13+
TabularDataset,
14+
Cluster,
15+
VectorItem,
16+
)
1117

1218

1319
class DatasetFilter(FilterSet):
@@ -42,3 +48,61 @@ class TabularDatasetFilter(DatasetFilter):
4248
class Meta:
4349
model = TabularDataset
4450
fields = ["name", "source", "cluster", "created", "updated"]
51+
52+
53+
class TabularItemFilter(FilterSet):
54+
filter = CharFilter(
55+
field_name="data",
56+
method="filter_metadata",
57+
help_text="""Filter by the content of the data JSONField.""",
58+
)
59+
60+
def split_values(self, value):
61+
return [
62+
[i.strip() for i in t.split("=")] # remove leading and ending spaces
63+
for t in value.split(",")
64+
if len(t.split("=")) == 2
65+
]
66+
67+
def filter_metadata(self, queryset, name, value):
68+
queries = self.split_values(value)
69+
70+
if not queries:
71+
return queryset
72+
73+
for key, val in queries:
74+
# For exact matching (current behavior)
75+
try:
76+
# Try numeric types
77+
if "." in val:
78+
filter_value = float(val)
79+
else:
80+
filter_value = int(val)
81+
except ValueError:
82+
# Handle booleans
83+
if val.lower() in ["true", "false"]:
84+
filter_value = val.lower() == "true"
85+
else:
86+
filter_value = val
87+
88+
# Use exact lookup
89+
lookup = f"{name}__{key}"
90+
queryset = queryset.filter(**{lookup: filter_value})
91+
92+
return queryset
93+
94+
class Meta:
95+
model = TabularItem
96+
fields = ["filter", "id"]
97+
98+
99+
class VectorItemFilter(TabularItemFilter):
100+
filter = CharFilter(
101+
field_name="metadata",
102+
method="filter_metadata",
103+
help_text="""Filter by the content of the metadata JSONField.""",
104+
)
105+
106+
class Meta:
107+
model = VectorItem
108+
fields = ["filter", "id"]

vbos/datasets/test/test_tabular_views.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,39 @@ def setUp(self):
8080
)
8181
TabularItem.objects.create(
8282
dataset=self.dataset_2,
83-
data={"employed_population": 0.75, "year": 2025, "month": 1},
83+
data={
84+
"employed_population": 0.75,
85+
"year": 2025,
86+
"month": 1,
87+
"region": "North",
88+
},
8489
)
8590
TabularItem.objects.create(
8691
dataset=self.dataset_2,
87-
data={"employed_population": 0.85, "year": 2024, "month": 7},
92+
data={
93+
"employed_population": 0.85,
94+
"year": 2024,
95+
"month": 7,
96+
"region": "North",
97+
},
8898
)
8999
TabularItem.objects.create(
90100
dataset=self.dataset_2,
91-
data={"employed_population": 0.82, "year": 2024, "month": 1},
101+
data={
102+
"employed_population": 0.82,
103+
"year": 2024,
104+
"month": 1,
105+
"region": "South",
106+
},
92107
)
93108
TabularItem.objects.create(
94109
dataset=self.dataset_2,
95-
data={"employed_population": 0.80, "year": 2023, "month": 7},
110+
data={
111+
"employed_population": 0.80,
112+
"year": 2023,
113+
"month": 7,
114+
"region": "East",
115+
},
96116
)
97117
self.url = reverse("datasets:tabular-data", args=[self.dataset_1.id])
98118

@@ -114,3 +134,21 @@ def test_tabular_datasets_data(self):
114134
assert req.data.get("results")[0]["employed_population"] == 0.75
115135
assert req.data.get("results")[0]["month"] == 1
116136
assert req.data.get("results")[0]["year"] == 2025
137+
138+
def test_filter_data(self):
139+
url = reverse("datasets:tabular-data", args=[self.dataset_2.id])
140+
req = self.client.get(url, {"filter": "year=2024"})
141+
assert req.status_code == status.HTTP_200_OK
142+
assert req.data.get("count") == 2
143+
144+
req = self.client.get(url, {"filter": "year=2024,month=1"})
145+
assert req.status_code == status.HTTP_200_OK
146+
assert req.data.get("count") == 1
147+
148+
req = self.client.get(url, {"filter": "year__gte=2024,region__icontains=south"})
149+
assert req.status_code == status.HTTP_200_OK
150+
assert req.data.get("count") == 1
151+
152+
req = self.client.get(url, {"filter": "region__icontains=north"})
153+
assert req.status_code == status.HTTP_200_OK
154+
assert req.data.get("count") == 2

vbos/datasets/test/test_vector_views.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,17 @@ def setUp(self):
6060
VectorItem.objects.create(
6161
dataset=self.dataset_1,
6262
geometry=Point(80.5, 10.232),
63-
metadata={"type": "administrative", "name": "Point 1"},
63+
metadata={"type": "administrative", "name": "Point 1", "area": 5000},
6464
)
6565
VectorItem.objects.create(
6666
dataset=self.dataset_1,
6767
geometry=LineString([(0, 0), (0, 3), (3, 3), (3, 0), (6, 6), (0, 0)]),
68-
metadata={"type": "administrative", "name": "Line 123"},
68+
metadata={"type": "administrative", "name": "Line 123", "area": 5321},
6969
)
7070
VectorItem.objects.create(
7171
dataset=self.dataset_2,
7272
geometry=Polygon([(0, 0), (0, 3), (3, 3), (3, 0), (0, 0)]),
73-
metadata={"type": "administrative", "name": "Area 1"},
73+
metadata={"type": "administrative", "name": "Area 1", "area": 3432},
7474
)
7575
self.url = reverse("datasets:vector-data", args=[self.dataset_1.id])
7676

@@ -109,3 +109,24 @@ def test_filters(self):
109109
"coordinates": [80.5, 10.232],
110110
}
111111
assert req.data.get("features")[0]["properties"]["name"] == "Point 1"
112+
113+
# filter by metadata
114+
req = self.client.get(self.url, {"filter": "name__icontains=Point"})
115+
assert req.status_code == status.HTTP_200_OK
116+
assert req.data.get("count") == 1
117+
118+
req = self.client.get(self.url, {"filter": "area__lt=5000"})
119+
assert req.status_code == status.HTTP_200_OK
120+
assert req.data.get("count") == 0
121+
122+
req = self.client.get(
123+
self.url, {"filter": "area__gte=5000, type=administrative"}
124+
)
125+
assert req.status_code == status.HTTP_200_OK
126+
assert req.data.get("count") == 2
127+
128+
req = self.client.get(
129+
self.url, {"filter": "area__gte=5000", "in_bbox": "80,10,81,11"}
130+
)
131+
assert req.status_code == status.HTTP_200_OK
132+
assert req.data.get("count") == 1

vbos/datasets/views.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.shortcuts import render
2+
import django_filters.rest_framework
23
from rest_framework.generics import ListAPIView, RetrieveAPIView
34
from rest_framework.permissions import IsAuthenticatedOrReadOnly
45
from rest_framework_gis.pagination import GeoJsonPagination
@@ -7,7 +8,9 @@
78
from vbos.datasets.filters import (
89
RasterDatasetFilter,
910
TabularDatasetFilter,
11+
TabularItemFilter,
1012
VectorDatasetFilter,
13+
VectorItemFilter,
1114
)
1215

1316
from .models import (
@@ -70,7 +73,11 @@ class VectorDatasetDataView(ListAPIView):
7073
permission_classes = [IsAuthenticatedOrReadOnly]
7174
pagination_class = GeoJsonPagination
7275
bbox_filter_field = "geometry"
73-
filter_backends = (InBBoxFilter,)
76+
filterset_class = VectorItemFilter
77+
filter_backends = (
78+
InBBoxFilter,
79+
django_filters.rest_framework.DjangoFilterBackend,
80+
)
7481

7582
def get_queryset(self):
7683
return VectorItem.objects.filter(dataset=self.kwargs.get("pk"))
@@ -91,6 +98,7 @@ class TabularDatasetDetailView(RetrieveAPIView):
9198

9299

93100
class TabularDatasetDataView(ListAPIView):
101+
filterset_class = TabularItemFilter
94102
permission_classes = [IsAuthenticatedOrReadOnly]
95103

96104
def get_queryset(self):

0 commit comments

Comments
 (0)