Skip to content

Commit c46b747

Browse files
committed
Add admin view to import VectorItem from geojson files
1 parent acae6c6 commit c46b747

File tree

6 files changed

+212
-5
lines changed

6 files changed

+212
-5
lines changed

vbos/datasets/admin.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import csv
2+
import json
23
from io import TextIOWrapper
34

45
from django.contrib.gis import admin
56
from django.contrib import messages
7+
from django.contrib.gis.geos.geometry import GEOSGeometry
68
from django.shortcuts import render, redirect, reverse
79
from django.urls import path
810

@@ -13,7 +15,7 @@
1315
VectorDataset,
1416
VectorItem,
1517
)
16-
from .forms import CSVUploadForm
18+
from .forms import CSVUploadForm, GeoJSONUploadForm
1719

1820

1921
@admin.register(RasterDataset)
@@ -28,7 +30,77 @@ class VectorDatasetAdmin(admin.ModelAdmin):
2830

2931
@admin.register(VectorItem)
3032
class VectorItemAdmin(admin.GISModelAdmin):
31-
list_display = ["id", "metadata"]
33+
list_display = ["id", "dataset", "metadata"]
34+
35+
def get_urls(self):
36+
urls = super().get_urls()
37+
custom_urls = [
38+
path(
39+
"upload-file/",
40+
self.admin_site.admin_view(self.import_file),
41+
name="datasets_vectoritem_import_file",
42+
),
43+
]
44+
return custom_urls + urls
45+
46+
def changelist_view(self, request, extra_context=None):
47+
extra_context = extra_context or {}
48+
extra_context["upload_file"] = reverse("admin:datasets_vectoritem_import_file")
49+
return super().changelist_view(request, extra_context=extra_context)
50+
51+
def import_file(self, request):
52+
if request.method == "POST":
53+
form = GeoJSONUploadForm(request.POST, request.FILES)
54+
if form.is_valid():
55+
uploaded_file = request.FILES["file"]
56+
57+
# Check if the file is a CSV
58+
if not uploaded_file.name.endswith(".geojson"):
59+
messages.error(request, "Please upload a GeoJSON file")
60+
return redirect("admin:datasets_vectoritem_import_file")
61+
62+
try:
63+
decoded_file = TextIOWrapper(uploaded_file.file, encoding="utf-8")
64+
geojson_content = json.loads(decoded_file.read())
65+
66+
created_count = 0
67+
error_count = 0
68+
69+
for item in geojson_content["features"]:
70+
try:
71+
VectorItem.objects.create(
72+
dataset=form.cleaned_data["dataset"],
73+
metadata=item["properties"],
74+
geometry=GEOSGeometry(json.dumps(item["geometry"])),
75+
)
76+
created_count += 1
77+
except Exception as e:
78+
print(e)
79+
error_count += 1
80+
81+
if created_count > 0:
82+
messages.success(
83+
request, f"Successfully created {created_count} new records"
84+
)
85+
86+
if error_count > 0:
87+
messages.warning(
88+
request, f"Failed to create {error_count} items."
89+
)
90+
91+
except Exception as e:
92+
messages.error(request, f"Error processing GeoJSON: {str(e)}")
93+
94+
return redirect("admin:datasets_vectoritem_import_file")
95+
else:
96+
form = GeoJSONUploadForm()
97+
98+
context = {
99+
"form": form,
100+
"opts": self.model._meta,
101+
"title": "Import GeoJSON File",
102+
}
103+
return render(request, "admin/file_upload.html", context)
32104

33105

34106
@admin.register(TabularDataset)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"type": "FeatureCollection",
3+
"features": [
4+
{
5+
"type": "Feature",
6+
"properties": {
7+
"name": "Area 1"
8+
},
9+
"geometry": {
10+
"coordinates": [
11+
[
12+
[
13+
167.59888,
14+
-16.28524
15+
],
16+
[
17+
167.59888,
18+
-16.35415
19+
],
20+
[
21+
167.65928,
22+
-16.35415
23+
],
24+
[
25+
167.65928,
26+
-16.28524
27+
],
28+
[
29+
167.59888,
30+
-16.28524
31+
]
32+
]
33+
],
34+
"type": "Polygon"
35+
}
36+
},
37+
{
38+
"type": "Feature",
39+
"properties": {"name": "Line 1", "col": "val"},
40+
"geometry": {
41+
"coordinates": [
42+
[
43+
167.471331,
44+
-16.249016
45+
],
46+
[
47+
167.500713,
48+
-16.372577
49+
]
50+
],
51+
"type": "LineString"
52+
}
53+
},
54+
{
55+
"type": "Feature",
56+
"properties": {"name": "Point 1", "source": "OpenStreetMap"},
57+
"geometry": {
58+
"coordinates": [
59+
167.72470,
60+
-16.39183
61+
],
62+
"type": "Point"
63+
}
64+
}
65+
]
66+
}

vbos/datasets/forms.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
from django import forms
2-
from .models import TabularDataset
2+
from .models import TabularDataset, VectorDataset
33

44

55
class CSVUploadForm(forms.Form):
66
file = forms.FileField(label="File")
77
dataset = forms.ModelChoiceField(
88
queryset=TabularDataset.objects.all(), empty_label="Select a dataset"
99
)
10+
11+
12+
class GeoJSONUploadForm(forms.Form):
13+
file = forms.FileField(label="File")
14+
dataset = forms.ModelChoiceField(
15+
queryset=VectorDataset.objects.all(), empty_label="Select a dataset"
16+
)

vbos/datasets/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class VectorItem(models.Model):
3232
metadata = models.JSONField(default=dict, blank=True, null=True)
3333

3434
def __str__(self):
35-
return self.id
35+
return f"{self.id}"
3636

3737
class Meta:
3838
ordering = ["id"]

vbos/datasets/test/test_admin.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.test import TestCase, Client
55
from django.urls import reverse
66

7-
from vbos.datasets.models import TabularDataset, TabularItem
7+
from vbos.datasets.models import TabularDataset, TabularItem, VectorDataset, VectorItem
88

99

1010
class TabularItemAdminImportFileTests(TestCase):
@@ -59,3 +59,55 @@ def test_post_valid_csv_creates_items(self):
5959
self.assertEqual(ti_2.dataset.id, self.dataset.id)
6060
self.assertEqual(ti_2.data["col1"], "val3")
6161
self.assertEqual(ti_2.data["col2"], "val4")
62+
63+
64+
class VectorItemAdminImportFileTests(TestCase):
65+
def setUp(self):
66+
self.client = Client()
67+
self.admin_user = get_user_model().objects.create_superuser(
68+
username="admin", password="password", email="[email protected]"
69+
)
70+
self.client.login(username="admin", password="password")
71+
self.dataset = VectorDataset.objects.create(name="Test Dataset")
72+
self.upload_url = reverse("admin:datasets_vectoritem_import_file")
73+
74+
def test_change_list_has_link_to_import_file(self):
75+
response = self.client.get(reverse("admin:datasets_vectoritem_changelist"))
76+
self.assertEqual(response.status_code, 200)
77+
self.assertContains(response, "Import File")
78+
79+
def test_get_import_file_view(self):
80+
response = self.client.get(self.upload_url)
81+
self.assertEqual(response.status_code, 200)
82+
self.assertContains(response, "Import GeoJSON File")
83+
self.assertContains(response, "Import File")
84+
self.assertContains(response, "Dataset")
85+
self.assertContains(response, "Test Dataset")
86+
87+
def test_post_invalid_file_type(self):
88+
file_data = io.BytesIO(b"not a geojson")
89+
file_data.name = "test.txt"
90+
response = self.client.post(
91+
self.upload_url,
92+
{"file": file_data, "dataset": self.dataset.id},
93+
follow=True,
94+
)
95+
self.assertContains(response, "Please upload a GeoJSON file")
96+
97+
def test_post_valid_geojson_creates_items(self):
98+
geojson_path = "./vbos/datasets/fixtures/test.geojson"
99+
with open(geojson_path, "rb") as file_data:
100+
response = self.client.post(
101+
self.upload_url,
102+
{"file": file_data, "dataset": self.dataset.id},
103+
follow=True,
104+
)
105+
self.assertContains(response, "Successfully created 3 new records")
106+
self.assertEqual(VectorItem.objects.count(), 3)
107+
vi_1, vi_2, vi_3 = VectorItem.objects.all()
108+
self.assertEqual(vi_1.dataset.id, self.dataset.id)
109+
self.assertEqual(vi_1.metadata["name"], "Area 1")
110+
self.assertEqual(vi_2.dataset.id, self.dataset.id)
111+
self.assertEqual(vi_2.metadata["name"], "Line 1")
112+
self.assertEqual(vi_3.dataset.id, self.dataset.id)
113+
self.assertEqual(vi_3.metadata["name"], "Point 1")
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 %}

0 commit comments

Comments
 (0)