Skip to content

Commit 8834370

Browse files
authored
Add Raster and Tabular data models and views (#2)
1 parent e50c658 commit 8834370

File tree

13 files changed

+464
-8
lines changed

13 files changed

+464
-8
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ djangorestframework-gis==1.2.0
1818
django-filter==24.3
1919
drf_spectacular==0.28.0
2020
django-cors-headers==4.7.0
21+
drf-excel==2.5.3
2122

2223
# Developer Tools
2324
ipdb==0.13.13

vbos/config/common.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,16 @@ class Common(Configuration):
182182
# Django Rest Framework
183183
REST_FRAMEWORK = {
184184
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
185-
"PAGE_SIZE": int(os.getenv("DJANGO_PAGINATION_LIMIT", 10)),
185+
"PAGE_SIZE": int(os.getenv("DJANGO_PAGINATION_LIMIT", 20)),
186186
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S%z",
187187
"DEFAULT_RENDERER_CLASSES": (
188188
"rest_framework.renderers.JSONRenderer",
189189
"rest_framework.renderers.BrowsableAPIRenderer",
190+
"drf_excel.renderers.XLSXRenderer",
190191
),
192+
"DEFAULT_FILTER_BACKENDS": [
193+
"django_filters.rest_framework.DjangoFilterBackend"
194+
],
191195
"DEFAULT_PERMISSION_CLASSES": [
192196
"rest_framework.permissions.IsAuthenticated",
193197
],

vbos/datasets/admin.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
from django.contrib.gis import admin
2-
from .models import VectorDataset, VectorItem
2+
from .models import (
3+
RasterDataset,
4+
TabularDataset,
5+
TabularItem,
6+
VectorDataset,
7+
VectorItem,
8+
)
9+
10+
11+
@admin.register(RasterDataset)
12+
class RasterDatasetAdmin(admin.ModelAdmin):
13+
list_display = ["id", "name", "created", "updated", "file_path"]
314

415

516
@admin.register(VectorDataset)
@@ -10,3 +21,13 @@ class VectorDatasetAdmin(admin.ModelAdmin):
1021
@admin.register(VectorItem)
1122
class VectorItemAdmin(admin.GISModelAdmin):
1223
list_display = ["id", "metadata"]
24+
25+
26+
@admin.register(TabularDataset)
27+
class TabularDatasetAdmin(admin.ModelAdmin):
28+
list_display = ["id", "name", "created", "updated"]
29+
30+
31+
@admin.register(TabularItem)
32+
class TabularItemAdmin(admin.GISModelAdmin):
33+
list_display = ["id", "data"]

vbos/datasets/filters.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django_filters import (
2+
FilterSet,
3+
BooleanFilter,
4+
CharFilter,
5+
OrderingFilter,
6+
DateFromToRangeFilter,
7+
ModelChoiceFilter,
8+
)
9+
10+
from .models import RasterDataset, VectorDataset, TabularDataset
11+
12+
13+
class DatasetFilter(FilterSet):
14+
name = CharFilter(field_name="name", lookup_expr="icontains")
15+
created = DateFromToRangeFilter()
16+
updated = DateFromToRangeFilter()
17+
order_by = OrderingFilter(
18+
fields=("name", "id", "updated", "created"),
19+
)
20+
21+
22+
class RasterDatasetFilter(DatasetFilter):
23+
class Meta:
24+
model = RasterDataset
25+
fields = ["name", "created", "updated"]
26+
27+
28+
class VectorDatasetFilter(DatasetFilter):
29+
class Meta:
30+
model = VectorDataset
31+
fields = ["name", "created", "updated"]
32+
33+
34+
class TabularDatasetFilter(DatasetFilter):
35+
class Meta:
36+
model = TabularDataset
37+
fields = ["name", "created", "updated"]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 5.2.5 on 2025-08-29 20:49
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("datasets", "0001_initial"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="TabularDataset",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("name", models.CharField(max_length=155)),
27+
("created", models.DateTimeField(auto_now_add=True)),
28+
("updated", models.DateTimeField(auto_now=True)),
29+
],
30+
options={
31+
"ordering": ["id"],
32+
},
33+
),
34+
migrations.CreateModel(
35+
name="TabularItem",
36+
fields=[
37+
(
38+
"id",
39+
models.AutoField(
40+
auto_created=True,
41+
primary_key=True,
42+
serialize=False,
43+
verbose_name="ID",
44+
),
45+
),
46+
("data", models.JSONField(default=dict)),
47+
(
48+
"dataset",
49+
models.ForeignKey(
50+
on_delete=django.db.models.deletion.CASCADE,
51+
to="datasets.tabulardataset",
52+
),
53+
),
54+
],
55+
options={
56+
"ordering": ["id"],
57+
},
58+
),
59+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 5.2.5 on 2025-08-29 21:22
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("datasets", "0002_tabulardataset_tabularitem"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="RasterDataset",
15+
fields=[
16+
(
17+
"id",
18+
models.AutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name="ID",
23+
),
24+
),
25+
("name", models.CharField(max_length=155)),
26+
("created", models.DateTimeField(auto_now_add=True)),
27+
("updated", models.DateTimeField(auto_now=True)),
28+
("file_path", models.CharField(max_length=2000)),
29+
],
30+
options={
31+
"ordering": ["id"],
32+
},
33+
),
34+
]

vbos/datasets/models.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
from django.contrib.gis.db import models
22

33

4+
class RasterDataset(models.Model):
5+
name = models.CharField(max_length=155)
6+
created = models.DateTimeField(auto_now_add=True)
7+
updated = models.DateTimeField(auto_now=True)
8+
file_path = models.CharField(max_length=2000, null=False, blank=False)
9+
10+
def __str__(self):
11+
return self.name
12+
13+
class Meta:
14+
ordering = ["id"]
15+
16+
417
class VectorDataset(models.Model):
518
name = models.CharField(max_length=155)
619
created = models.DateTimeField(auto_now_add=True)
@@ -23,3 +36,26 @@ def __str__(self):
2336

2437
class Meta:
2538
ordering = ["id"]
39+
40+
41+
class TabularDataset(models.Model):
42+
name = models.CharField(max_length=155)
43+
created = models.DateTimeField(auto_now_add=True)
44+
updated = models.DateTimeField(auto_now=True)
45+
46+
def __str__(self):
47+
return self.name
48+
49+
class Meta:
50+
ordering = ["id"]
51+
52+
53+
class TabularItem(models.Model):
54+
dataset = models.ForeignKey(TabularDataset, on_delete=models.CASCADE)
55+
data = models.JSONField(default=dict)
56+
57+
def __str__(self):
58+
return f"{self.id}"
59+
60+
class Meta:
61+
ordering = ["id"]

vbos/datasets/serializers.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
from rest_framework import serializers
22
from rest_framework_gis.serializers import GeoFeatureModelSerializer
33

4-
from .models import VectorDataset, VectorItem
4+
from .models import (
5+
RasterDataset,
6+
TabularDataset,
7+
TabularItem,
8+
VectorDataset,
9+
VectorItem,
10+
)
11+
12+
13+
class RasterDatasetSerializer(serializers.ModelSerializer):
14+
class Meta:
15+
model = RasterDataset
16+
fields = "__all__"
517

618

719
class VectorDatasetSerializer(serializers.ModelSerializer):
@@ -30,3 +42,47 @@ def unformat_geojson(self, feature):
3042
attrs[self.Meta.bbox_geo_field] = Polygon.from_bbox(feature["bbox"])
3143

3244
return attrs
45+
46+
47+
class TabularDatasetSerializer(serializers.ModelSerializer):
48+
class Meta:
49+
model = TabularDataset
50+
fields = "__all__"
51+
52+
53+
class TabularItemSerializer(serializers.ModelSerializer):
54+
class Meta:
55+
model = TabularItem
56+
fields = ["id", "data"]
57+
58+
def to_representation(self, instance):
59+
representation = super().to_representation(instance)
60+
61+
# Extract the data field and merge it with the top level fields
62+
data_content = representation.pop("data", {})
63+
64+
return {**representation, **data_content}
65+
66+
67+
class TabularItemExcelSerializer(serializers.ModelSerializer):
68+
# Dynamically add fields based on all possible keys in the data
69+
def __init__(self, *args, **kwargs):
70+
super().__init__(*args, **kwargs)
71+
72+
# Get all possible keys from the queryset
73+
if self.context.get("view"):
74+
queryset = self.context["view"].get_queryset()
75+
all_keys = set()
76+
for item in queryset:
77+
if item.data and isinstance(item.data, dict):
78+
all_keys.update(item.data.keys())
79+
80+
# Create a field for each key
81+
for key in all_keys:
82+
self.fields[key] = serializers.CharField(
83+
source=f"data.{key}", required=False, allow_blank=True, default=""
84+
)
85+
86+
class Meta:
87+
model = TabularItem
88+
fields = ["id"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from rest_framework import status
2+
from rest_framework.test import APITestCase
3+
from django.urls import reverse
4+
5+
from ..models import RasterDataset
6+
7+
8+
class TestRasterDatasetListDetailViews(APITestCase):
9+
def setUp(self):
10+
self.dataset_1 = RasterDataset.objects.create(
11+
name="Rainfall", file_path="cogs/rainfall.tiff"
12+
)
13+
self.dataset_2 = RasterDataset.objects.create(
14+
name="Coastline changes", file_path="cogs/coastlines.tiff"
15+
)
16+
self.url = reverse("datasets:raster-list")
17+
18+
def test_raster_datasets_list(self):
19+
req = self.client.get(self.url)
20+
assert req.status_code == status.HTTP_200_OK
21+
assert req.data.get("count") == 2
22+
assert req.data.get("results")[0]["name"] == "Rainfall"
23+
assert req.data.get("results")[1]["name"] == "Coastline changes"
24+
25+
def test_raster_datasets_detail(self):
26+
url = reverse("datasets:raster-detail", args=[self.dataset_1.id])
27+
req = self.client.get(url)
28+
assert req.status_code == status.HTTP_200_OK
29+
assert req.data.get("name") == "Rainfall"
30+
assert req.data.get("file_path") == "cogs/rainfall.tiff"
31+
assert req.data.get("created")
32+
assert req.data.get("updated")

0 commit comments

Comments
 (0)