Skip to content

Commit acae6c6

Browse files
authored
Add admin view to Import Tabular Items from a CSV file (#3)
* Add admin view to Import Tabular Items from a CSV file * Lint code
1 parent 8834370 commit acae6c6

File tree

7 files changed

+202
-2
lines changed

7 files changed

+202
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ venv.bak/
105105

106106
# mypy
107107
.mypy_cache/
108+
109+
# macos
110+
.DS_Store

vbos/config/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class Common(Configuration):
8888
TEMPLATES = [
8989
{
9090
"BACKEND": "django.template.backends.django.DjangoTemplates",
91-
"DIRS": STATICFILES_DIRS,
91+
"DIRS": [os.path.join(BASE_DIR, "templates")],
9292
"APP_DIRS": True,
9393
"OPTIONS": {
9494
"context_processors": [

vbos/datasets/admin.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
import csv
2+
from io import TextIOWrapper
3+
14
from django.contrib.gis import admin
5+
from django.contrib import messages
6+
from django.shortcuts import render, redirect, reverse
7+
from django.urls import path
8+
29
from .models import (
310
RasterDataset,
411
TabularDataset,
512
TabularItem,
613
VectorDataset,
714
VectorItem,
815
)
16+
from .forms import CSVUploadForm
917

1018

1119
@admin.register(RasterDataset)
@@ -30,4 +38,73 @@ class TabularDatasetAdmin(admin.ModelAdmin):
3038

3139
@admin.register(TabularItem)
3240
class TabularItemAdmin(admin.GISModelAdmin):
33-
list_display = ["id", "data"]
41+
list_display = ["id", "dataset", "data"]
42+
43+
def get_urls(self):
44+
urls = super().get_urls()
45+
custom_urls = [
46+
path(
47+
"upload-file/",
48+
self.admin_site.admin_view(self.import_file),
49+
name="datasets_tabularitem_import_file",
50+
),
51+
]
52+
return custom_urls + urls
53+
54+
def changelist_view(self, request, extra_context=None):
55+
extra_context = extra_context or {}
56+
extra_context["upload_file"] = reverse("admin:datasets_tabularitem_import_file")
57+
return super().changelist_view(request, extra_context=extra_context)
58+
59+
def import_file(self, request):
60+
if request.method == "POST":
61+
form = CSVUploadForm(request.POST, request.FILES)
62+
if form.is_valid():
63+
uploaded_file = request.FILES["file"]
64+
65+
# Check if the file is a CSV
66+
if not uploaded_file.name.endswith(".csv"):
67+
messages.error(request, "Please upload a CSV file")
68+
return redirect("admin:datasets_tabularitem_import_file")
69+
70+
try:
71+
# Read and process the CSV
72+
decoded_file = TextIOWrapper(uploaded_file.file, encoding="utf-8")
73+
reader = csv.DictReader(decoded_file)
74+
75+
created_count = 0
76+
error_count = 0
77+
78+
for row in reader: # start=2 to account for header row
79+
try:
80+
TabularItem.objects.create(
81+
dataset=form.cleaned_data["dataset"], data=row
82+
)
83+
created_count += 1
84+
except Exception as e:
85+
print(e)
86+
error_count += 1
87+
88+
if created_count > 0:
89+
messages.success(
90+
request, f"Successfully created {created_count} new records"
91+
)
92+
93+
if error_count > 0:
94+
messages.warning(
95+
request, f"Failed to create {error_count} items."
96+
)
97+
98+
except Exception as e:
99+
messages.error(request, f"Error processing CSV: {str(e)}")
100+
101+
return redirect("admin:datasets_tabularitem_import_file")
102+
else:
103+
form = CSVUploadForm()
104+
105+
context = {
106+
"form": form,
107+
"opts": self.model._meta,
108+
"title": "Import CSV File",
109+
}
110+
return render(request, "admin/file_upload.html", context)

vbos/datasets/forms.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django import forms
2+
from .models import TabularDataset
3+
4+
5+
class CSVUploadForm(forms.Form):
6+
file = forms.FileField(label="File")
7+
dataset = forms.ModelChoiceField(
8+
queryset=TabularDataset.objects.all(), empty_label="Select a dataset"
9+
)

vbos/datasets/test/test_admin.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import io
2+
3+
from django.contrib.auth import get_user_model
4+
from django.test import TestCase, Client
5+
from django.urls import reverse
6+
7+
from vbos.datasets.models import TabularDataset, TabularItem
8+
9+
10+
class TabularItemAdminImportFileTests(TestCase):
11+
def setUp(self):
12+
self.client = Client()
13+
self.admin_user = get_user_model().objects.create_superuser(
14+
username="admin", password="password", email="[email protected]"
15+
)
16+
self.client.login(username="admin", password="password")
17+
self.dataset = TabularDataset.objects.create(name="Test Dataset")
18+
self.upload_url = reverse("admin:datasets_tabularitem_import_file")
19+
20+
def test_change_list_has_link_to_import_file(self):
21+
response = self.client.get(reverse("admin:datasets_tabularitem_changelist"))
22+
self.assertEqual(response.status_code, 200)
23+
self.assertContains(response, "Import File")
24+
25+
def test_get_import_file_view(self):
26+
response = self.client.get(self.upload_url)
27+
self.assertEqual(response.status_code, 200)
28+
self.assertContains(response, "Import CSV File")
29+
self.assertContains(response, "Import File")
30+
self.assertContains(response, "Dataset")
31+
self.assertContains(response, "Test Dataset")
32+
33+
def test_post_invalid_file_type(self):
34+
file_data = io.BytesIO(b"not a csv")
35+
file_data.name = "test.txt"
36+
response = self.client.post(
37+
self.upload_url,
38+
{"file": file_data, "dataset": self.dataset.id},
39+
follow=True,
40+
)
41+
self.assertContains(response, "Please upload a CSV file")
42+
43+
def test_post_valid_csv_creates_items(self):
44+
csv_content = "col1,col2\nval1,val2\nval3,val4\n"
45+
file_data = io.BytesIO(csv_content.encode("utf-8"))
46+
file_data.name = "test.csv"
47+
response = self.client.post(
48+
self.upload_url,
49+
{"file": file_data, "dataset": self.dataset.id},
50+
follow=True,
51+
)
52+
self.assertContains(response, "Successfully created 2 new records")
53+
self.assertEqual(TabularItem.objects.count(), 2)
54+
ti_1 = TabularItem.objects.first()
55+
self.assertEqual(ti_1.dataset.id, self.dataset.id)
56+
self.assertEqual(ti_1.data["col1"], "val1")
57+
self.assertEqual(ti_1.data["col2"], "val2")
58+
ti_2 = TabularItem.objects.last()
59+
self.assertEqual(ti_2.dataset.id, self.dataset.id)
60+
self.assertEqual(ti_2.data["col1"], "val3")
61+
self.assertEqual(ti_2.data["col2"], "val4")
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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!-- templates/admin/file_upload.html -->
2+
{% extends "admin/base_site.html" %}
3+
{% load i18n admin_urls static %}
4+
{% block content %}
5+
<div id="content-main">
6+
<form method="post" enctype="multipart/form-data">
7+
{% csrf_token %}
8+
9+
<div class="form-row">
10+
{{ form.file.label_tag }}
11+
{{ form.file }}
12+
<div>
13+
<p>
14+
{{ form.dataset.label_tag }}
15+
</p>
16+
<p>
17+
{{ form.dataset }}
18+
</p>
19+
</div>
20+
{% if form.csv_file.errors %}
21+
<div class="error">
22+
{{ form.csv_file.errors }}
23+
</div>
24+
{% endif %}
25+
</div>
26+
27+
<div class="submit-row">
28+
<input
29+
type="submit"
30+
value="{% trans 'Import File' %}"
31+
class="default"
32+
/>
33+
</div>
34+
</form>
35+
36+
{% if form.errors %}
37+
<div class="errornote">{% trans "Please correct the errors below." %}</div>
38+
{% endif %}
39+
</div>
40+
{% endblock %}

0 commit comments

Comments
 (0)