Skip to content

Commit 46304b3

Browse files
authored
Add RasterFile model and use it as Foreing Key to RasterDataset (#5)
* Add RasterFile model and use it as Foreing Key to RasterDataset * Improve validation and tests
1 parent e422232 commit 46304b3

File tree

9 files changed

+170
-42
lines changed

9 files changed

+170
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ coverage.xml
5454
*.log
5555
.static_storage/
5656
.media/
57+
.media/raster/*
5758
local_settings.py
5859

5960
# Flask stuff:

vbos/datasets/admin.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .models import (
1212
RasterDataset,
13+
RasterFile,
1314
TabularDataset,
1415
TabularItem,
1516
VectorDataset,
@@ -18,9 +19,14 @@
1819
from .forms import CSVUploadForm, GeoJSONUploadForm
1920

2021

22+
@admin.register(RasterFile)
23+
class RasterFileAdmin(admin.ModelAdmin):
24+
list_display = ["id", "name", "created", "file"]
25+
26+
2127
@admin.register(RasterDataset)
2228
class RasterDatasetAdmin(admin.ModelAdmin):
23-
list_display = ["id", "name", "created", "updated", "file_path"]
29+
list_display = ["id", "name", "created", "updated", "file"]
2430

2531

2632
@admin.register(VectorDataset)

vbos/datasets/migrations/0003_rasterdataset.py

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Generated by Django 5.2.5 on 2025-09-03 16:50
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", "0002_tabulardataset_tabularitem"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="RasterFile",
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+
("file", models.FileField(upload_to="raster/")),
29+
],
30+
options={
31+
"ordering": ["id"],
32+
},
33+
),
34+
migrations.CreateModel(
35+
name="RasterDataset",
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+
("name", models.CharField(max_length=155)),
47+
("created", models.DateTimeField(auto_now_add=True)),
48+
("updated", models.DateTimeField(auto_now=True)),
49+
(
50+
"file",
51+
models.ForeignKey(
52+
on_delete=django.db.models.deletion.PROTECT,
53+
to="datasets.rasterfile",
54+
),
55+
),
56+
],
57+
options={
58+
"ordering": ["id"],
59+
},
60+
),
61+
]

vbos/datasets/models.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
11
from django.contrib.gis.db import models
2+
from django.core.validators import FileExtensionValidator
3+
from django.db.models.fields.files import default_storage
4+
from django.db.models.signals import pre_delete
5+
from django.dispatch import receiver
6+
7+
8+
class RasterFile(models.Model):
9+
name = models.CharField(max_length=155)
10+
created = models.DateTimeField(auto_now_add=True)
11+
file = models.FileField(
12+
upload_to="raster/",
13+
validators=[
14+
FileExtensionValidator(
15+
allowed_extensions=["tiff", "tif", "geotiff", "gtiff"]
16+
)
17+
],
18+
)
19+
20+
def __str__(self):
21+
return self.name
22+
23+
class Meta:
24+
ordering = ["id"]
25+
26+
27+
@receiver(pre_delete, sender=RasterFile)
28+
def delete_raster_file(sender, instance, **kwargs):
29+
"""
30+
Delete the file from storage when a RasterFile instance is deleted
31+
"""
32+
if instance.file:
33+
# Using default_storage for better compatibility with different storage backends
34+
if default_storage.exists(instance.file.name):
35+
default_storage.delete(instance.file.name)
236

337

438
class RasterDataset(models.Model):
539
name = models.CharField(max_length=155)
640
created = models.DateTimeField(auto_now_add=True)
741
updated = models.DateTimeField(auto_now=True)
8-
file_path = models.CharField(max_length=2000, null=False, blank=False)
42+
file = models.ForeignKey(RasterFile, on_delete=models.PROTECT)
943

1044
def __str__(self):
1145
return self.name

vbos/datasets/serializers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212

1313
class RasterDatasetSerializer(serializers.ModelSerializer):
14+
file = serializers.ReadOnlyField(source="file.file.url")
15+
1416
class Meta:
1517
model = RasterDataset
16-
fields = "__all__"
18+
fields = ["id", "name", "created", "updated", "file"]
1719

1820

1921
class VectorDatasetSerializer(serializers.ModelSerializer):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% extends "admin/change_list.html" %}
2+
{% load admin_urls %}
3+
{% block object-tools-items %}
4+
{{ block.super }}
5+
<li>
6+
<a href="{% url opts|admin_urlname:'import_file' %}" class="addlink">
7+
Import File
8+
</a>
9+
</li>
10+
{% endblock %}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from django.db.models.deletion import ProtectedError
2+
from django.test import TestCase
3+
from django.core.files.uploadedfile import SimpleUploadedFile
4+
from django.core.exceptions import ValidationError
5+
6+
from vbos.datasets.models import RasterDataset, RasterFile
7+
from genericpath import exists
8+
9+
10+
class TestRasterModels(TestCase):
11+
def setUp(self):
12+
self.valid_file = SimpleUploadedFile(
13+
"rainfall.tif", b"file_content", content_type="image/tiff"
14+
)
15+
self.r_1 = RasterFile.objects.create(name="Rainfall COG", file=self.valid_file)
16+
self.r_2 = RasterFile.objects.create(
17+
name="Coastline COG", file="raster/coastline.tiff"
18+
)
19+
self.dataset = RasterDataset.objects.create(name="Rainfall", file=self.r_1)
20+
21+
def test_deletion(self):
22+
# RasterFile can't be deleted if it's associates with a dataset
23+
with self.assertRaises(ProtectedError):
24+
self.r_1.delete()
25+
# modify dataset
26+
self.dataset.file = self.r_2
27+
self.dataset.save()
28+
# delete file
29+
self.r_1.delete()
30+
self.assertEqual(RasterFile.objects.count(), 1)
31+
# delete dataset
32+
self.dataset.delete()
33+
self.assertEqual(RasterDataset.objects.count(), 0)
34+
# delete remaining file
35+
self.r_2.delete()
36+
self.assertEqual(RasterFile.objects.count(), 0)
37+
38+
# test file extension validation
39+
invalid_file = SimpleUploadedFile(
40+
"test.jpg", b"file_content", content_type="image/jpeg"
41+
)
42+
raster = RasterFile(name="Test", file=invalid_file)
43+
with self.assertRaises(ValidationError):
44+
raster.full_clean()

vbos/datasets/test/test_raster_views.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
from rest_framework.test import APITestCase
33
from django.urls import reverse
44

5-
from ..models import RasterDataset
5+
from ..models import RasterDataset, RasterFile
66

77

88
class TestRasterDatasetListDetailViews(APITestCase):
99
def setUp(self):
10-
self.dataset_1 = RasterDataset.objects.create(
11-
name="Rainfall", file_path="cogs/rainfall.tiff"
10+
self.r_1 = RasterFile.objects.create(
11+
name="Rainfall COG", file="raster/rainfall.tiff"
1212
)
13+
self.r_2 = RasterFile.objects.create(
14+
name="Coastline COG", file="raster/coastline.tiff"
15+
)
16+
self.dataset_1 = RasterDataset.objects.create(name="Rainfall", file=self.r_1)
1317
self.dataset_2 = RasterDataset.objects.create(
14-
name="Coastline changes", file_path="cogs/coastlines.tiff"
18+
name="Coastline changes", file=self.r_2
1519
)
1620
self.url = reverse("datasets:raster-list")
1721

@@ -27,6 +31,6 @@ def test_raster_datasets_detail(self):
2731
req = self.client.get(url)
2832
assert req.status_code == status.HTTP_200_OK
2933
assert req.data.get("name") == "Rainfall"
30-
assert req.data.get("file_path") == "cogs/rainfall.tiff"
34+
assert req.data.get("file") == "/media/raster/rainfall.tiff"
3135
assert req.data.get("created")
3236
assert req.data.get("updated")

0 commit comments

Comments
 (0)