Skip to content

Commit f077203

Browse files
authored
Merge pull request #88 from silvioheinze/delete-geometry-function
Add geometry deletion functionality and related tests
2 parents b8f1041 + 48718ea commit f077203

7 files changed

Lines changed: 233 additions & 7 deletions

File tree

app/datasets/tests/test_data_input_js.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def test_geometry_details_api_returns_correct_data(self):
203203
geometry_data = data['geometry']
204204
self.assertEqual(geometry_data['id'], self.geometry.id)
205205
self.assertEqual(geometry_data['id_kurz'], 'TEST001')
206+
self.assertEqual(geometry_data['created_by_user_id'], self.user.id)
206207
self.assertIn('entries', geometry_data)
207208

208209
# Check entries structure
@@ -288,6 +289,7 @@ def test_map_data_api_returns_lightweight_data(self):
288289
self.assertIn('lat', point)
289290
self.assertIn('lng', point)
290291
self.assertIn('user', point)
292+
self.assertEqual(point['created_by_user_id'], self.user.id)
291293

292294
# Should not contain entries or field data
293295
self.assertNotIn('entries', point)
@@ -416,7 +418,7 @@ def test_geometry_details_api_structure_matches_js_expectations(self):
416418
self.assertIn('geometry', data)
417419

418420
geometry = data['geometry']
419-
required_fields = ['id', 'id_kurz', 'address', 'lat', 'lng', 'user', 'entries']
421+
required_fields = ['id', 'id_kurz', 'address', 'lat', 'lng', 'user', 'created_by_user_id', 'entries']
420422
for field in required_fields:
421423
self.assertIn(field, geometry, f"Required field {field} missing from geometry data")
422424

@@ -573,3 +575,47 @@ def test_javascript_non_editable_fields_in_json_data(self):
573575
# This ensures the property name matches what JavaScript expects
574576
self.assertIn('non_editable', non_editable_data, "non_editable property should be in JSON")
575577
self.assertEqual(non_editable_data['non_editable'], True, "non_editable should be True in JSON")
578+
579+
def test_owner_can_delete_geometry_via_post(self):
580+
"""Dataset owner can delete a geometry; it is removed from the database."""
581+
client = Client()
582+
client.force_login(self.user)
583+
gid = self.geometry.id
584+
url = reverse('geometry_delete', kwargs={'geometry_id': gid})
585+
response = client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
586+
self.assertEqual(response.status_code, 200)
587+
self.assertTrue(response.json().get('success'))
588+
self.assertFalse(DataGeometry.objects.filter(pk=gid).exists())
589+
590+
def test_shared_user_cannot_delete_others_geometry(self):
591+
"""Collaborators cannot delete geometries created by another user."""
592+
other = User.objects.create_user(username='collab', email='c@example.com', password='pass')
593+
self.dataset.shared_with.add(other)
594+
client = Client()
595+
client.force_login(other)
596+
gid = self.geometry.id
597+
url = reverse('geometry_delete', kwargs={'geometry_id': gid})
598+
response = client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
599+
self.assertEqual(response.status_code, 403)
600+
self.assertTrue(DataGeometry.objects.filter(pk=gid).exists())
601+
602+
def test_creator_can_delete_own_geometry(self):
603+
"""A logged-in user can delete a geometry they created."""
604+
from django.contrib.gis.geos import Point
605+
606+
collab = User.objects.create_user(username='collab2', email='c2@example.com', password='pass')
607+
self.dataset.shared_with.add(collab)
608+
own = DataGeometry.objects.create(
609+
dataset=self.dataset,
610+
id_kurz='OWN001',
611+
address='Own point',
612+
geometry=Point(16.0, 48.1),
613+
user=collab,
614+
)
615+
client = Client()
616+
client.force_login(collab)
617+
url = reverse('geometry_delete', kwargs={'geometry_id': own.id})
618+
response = client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
619+
self.assertEqual(response.status_code, 200)
620+
self.assertTrue(response.json().get('success'))
621+
self.assertFalse(DataGeometry.objects.filter(pk=own.id).exists())

app/datasets/views/dataset_views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,8 @@ def dataset_map_data_view(request, dataset_id):
11431143
'address': geometry.address,
11441144
'lat': geometry.geometry.y,
11451145
'lng': geometry.geometry.x,
1146-
'user': geometry.get_creator_display_name()
1146+
'user': geometry.get_creator_display_name(),
1147+
'created_by_user_id': geometry.user_id,
11471148
}
11481149
map_data.append(map_point)
11491150
except Exception:

app/datasets/views/geometry_views.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def geometry_details_view(request, geometry_id):
126126
'lat': geometry.geometry.y,
127127
'lng': geometry.geometry.x,
128128
'user': geometry.get_creator_display_name(),
129+
'created_by_user_id': geometry.user_id,
129130
'entries': []
130131
}
131132
for entry in geometry.entries.all():
@@ -155,3 +156,26 @@ def geometry_details_view(request, geometry_id):
155156

156157
except Exception as e:
157158
return JsonResponse({'success': False, 'error': str(e)}, status=500)
159+
160+
161+
@login_required
162+
def geometry_delete_view(request, geometry_id):
163+
"""Delete a geometry point (dataset owner, superuser, or logged-in creator with area access)."""
164+
if request.method != 'POST':
165+
return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
166+
try:
167+
geometry = get_object_or_404(DataGeometry, pk=geometry_id)
168+
dataset = geometry.dataset
169+
if not dataset.can_access(request.user):
170+
return JsonResponse({'success': False, 'error': 'Access denied'}, status=403)
171+
if request.user.is_superuser or request.user == dataset.owner:
172+
geometry.delete()
173+
return JsonResponse({'success': True})
174+
if geometry.user_id == request.user.id:
175+
if not dataset.user_has_geometry_access(request.user, geometry):
176+
return JsonResponse({'success': False, 'error': 'Access denied'}, status=403)
177+
geometry.delete()
178+
return JsonResponse({'success': True})
179+
return JsonResponse({'success': False, 'error': 'Access denied'}, status=403)
180+
except Exception as e:
181+
return JsonResponse({'success': False, 'error': str(e)}, status=500)

app/isrfield/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
path('datasets/<int:dataset_id>/fields/', datasets_views.dataset_fields_view, name='dataset_fields'),
7171
path('datasets/<int:dataset_id>/map-data/', datasets_views.dataset_map_data_view, name='dataset_map_data'),
7272
path('datasets/geometry/<int:geometry_id>/details/', datasets_views.geometry_details_view, name='geometry_details'),
73+
path('datasets/geometry/<int:geometry_id>/delete/', datasets_views.geometry_delete_view, name='geometry_delete'),
7374
path('datasets/<int:dataset_id>/clear-data/', datasets_views.dataset_clear_data_view, name='dataset_clear_data'),
7475
path('datasets/<int:dataset_id>/geometries/create/', datasets_views.geometry_create_view, name='geometry_create'),
7576
path('entries/<int:entry_id>/edit/', datasets_views.entry_edit_view, name='entry_edit'),

app/static/js/data-input.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,75 @@ function showGeometryDetails(point) {
307307
detailsDiv.classList.add('active');
308308
generateEntriesTable(point);
309309
loadUploadedFiles();
310+
updateDeleteGeometryButton();
310311
if (typeof adjustColumnLayout === 'function') adjustColumnLayout();
311312
}
312313

314+
function updateDeleteGeometryButton() {
315+
var btn = document.getElementById('deleteGeometryBtn');
316+
if (!btn) return;
317+
var uid = window.currentUserId;
318+
var creatorId = currentPoint && currentPoint.created_by_user_id;
319+
var isCreator = uid != null && creatorId != null &&
320+
Number(creatorId) === Number(uid);
321+
var canDelete = window.isDatasetOwner ||
322+
(currentPoint && currentPoint.id != null && isCreator);
323+
if (canDelete) {
324+
btn.classList.remove('d-none');
325+
btn.setAttribute('data-geometry-id', String(currentPoint.id));
326+
} else {
327+
btn.classList.add('d-none');
328+
btn.removeAttribute('data-geometry-id');
329+
}
330+
}
331+
332+
function deleteCurrentGeometry() {
333+
var btn = document.getElementById('deleteGeometryBtn');
334+
if (!btn) return;
335+
var uid = window.currentUserId;
336+
var creatorId = currentPoint && currentPoint.created_by_user_id;
337+
var isCreator = uid != null && creatorId != null &&
338+
Number(creatorId) === Number(uid);
339+
if (!window.isDatasetOwner && !isCreator) return;
340+
var gid = btn.getAttribute('data-geometry-id');
341+
if (!gid) return;
342+
var msg = (window.translations && window.translations.deleteGeometryConfirm) ?
343+
window.translations.deleteGeometryConfirm :
344+
'Delete this geometry point and all its entries? This cannot be undone.';
345+
if (!confirm(msg)) return;
346+
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
347+
if (!csrfEl || !csrfEl.value) {
348+
alert((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
349+
return;
350+
}
351+
fetch(window.location.origin + '/datasets/geometry/' + gid + '/delete/', {
352+
method: 'POST',
353+
credentials: 'same-origin',
354+
headers: {
355+
'X-CSRFToken': csrfEl.value,
356+
'X-Requested-With': 'XMLHttpRequest'
357+
}
358+
})
359+
.then(function(response) {
360+
return response.json().then(function(data) {
361+
return { ok: response.ok, data: data };
362+
});
363+
})
364+
.then(function(result) {
365+
if (result.ok && result.data && result.data.success) {
366+
clearSelection();
367+
loadMapData(true);
368+
} else {
369+
var err = (result.data && result.data.error) ? result.data.error :
370+
((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
371+
alert(err);
372+
}
373+
})
374+
.catch(function() {
375+
alert((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
376+
});
377+
}
378+
313379
// Global variable to track selected entry
314380
var selectedEntryId = null;
315381

@@ -1738,6 +1804,12 @@ function clearSelection() {
17381804
if (typeof adjustColumnLayout === 'function') {
17391805
adjustColumnLayout();
17401806
}
1807+
1808+
var deleteBtn = document.getElementById('deleteGeometryBtn');
1809+
if (deleteBtn) {
1810+
deleteBtn.classList.add('d-none');
1811+
deleteBtn.removeAttribute('data-geometry-id');
1812+
}
17411813
}
17421814

17431815
// Get dataset ID from URL

app/staticfiles/js/data-input.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,75 @@ function showGeometryDetails(point) {
307307
detailsDiv.classList.add('active');
308308
generateEntriesTable(point);
309309
loadUploadedFiles();
310+
updateDeleteGeometryButton();
310311
if (typeof adjustColumnLayout === 'function') adjustColumnLayout();
311312
}
312313

314+
function updateDeleteGeometryButton() {
315+
var btn = document.getElementById('deleteGeometryBtn');
316+
if (!btn) return;
317+
var uid = window.currentUserId;
318+
var creatorId = currentPoint && currentPoint.created_by_user_id;
319+
var isCreator = uid != null && creatorId != null &&
320+
Number(creatorId) === Number(uid);
321+
var canDelete = window.isDatasetOwner ||
322+
(currentPoint && currentPoint.id != null && isCreator);
323+
if (canDelete) {
324+
btn.classList.remove('d-none');
325+
btn.setAttribute('data-geometry-id', String(currentPoint.id));
326+
} else {
327+
btn.classList.add('d-none');
328+
btn.removeAttribute('data-geometry-id');
329+
}
330+
}
331+
332+
function deleteCurrentGeometry() {
333+
var btn = document.getElementById('deleteGeometryBtn');
334+
if (!btn) return;
335+
var uid = window.currentUserId;
336+
var creatorId = currentPoint && currentPoint.created_by_user_id;
337+
var isCreator = uid != null && creatorId != null &&
338+
Number(creatorId) === Number(uid);
339+
if (!window.isDatasetOwner && !isCreator) return;
340+
var gid = btn.getAttribute('data-geometry-id');
341+
if (!gid) return;
342+
var msg = (window.translations && window.translations.deleteGeometryConfirm) ?
343+
window.translations.deleteGeometryConfirm :
344+
'Delete this geometry point and all its entries? This cannot be undone.';
345+
if (!confirm(msg)) return;
346+
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
347+
if (!csrfEl || !csrfEl.value) {
348+
alert((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
349+
return;
350+
}
351+
fetch(window.location.origin + '/datasets/geometry/' + gid + '/delete/', {
352+
method: 'POST',
353+
credentials: 'same-origin',
354+
headers: {
355+
'X-CSRFToken': csrfEl.value,
356+
'X-Requested-With': 'XMLHttpRequest'
357+
}
358+
})
359+
.then(function(response) {
360+
return response.json().then(function(data) {
361+
return { ok: response.ok, data: data };
362+
});
363+
})
364+
.then(function(result) {
365+
if (result.ok && result.data && result.data.success) {
366+
clearSelection();
367+
loadMapData(true);
368+
} else {
369+
var err = (result.data && result.data.error) ? result.data.error :
370+
((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
371+
alert(err);
372+
}
373+
})
374+
.catch(function() {
375+
alert((window.translations && window.translations.deleteGeometryFailed) || 'Could not delete geometry.');
376+
});
377+
}
378+
313379
// Global variable to track selected entry
314380
var selectedEntryId = null;
315381

@@ -1738,6 +1804,12 @@ function clearSelection() {
17381804
if (typeof adjustColumnLayout === 'function') {
17391805
adjustColumnLayout();
17401806
}
1807+
1808+
var deleteBtn = document.getElementById('deleteGeometryBtn');
1809+
if (deleteBtn) {
1810+
deleteBtn.classList.add('d-none');
1811+
deleteBtn.removeAttribute('data-geometry-id');
1812+
}
17411813
}
17421814

17431815
// Get dataset ID from URL

app/templates/datasets/dataset_data_input.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@
1919

2020
<!-- Geometry Details Overlay/Inline -->
2121
<div id="geometryDetails" class="geometry-details">
22-
<div class="d-flex justify-content-between align-items-center mb-3">
22+
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
2323
<h5 class="mb-0">{% trans "Geometry Details" %}</h5>
24-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSelection()" title="{% trans 'Clear Selection' %}">
25-
<i class="bi bi-x-circle"></i> {% trans "Close" %}
26-
</button>
24+
<div class="d-flex align-items-center gap-2 ms-auto">
25+
{% if user.is_authenticated %}
26+
<button type="button" id="deleteGeometryBtn" class="btn btn-sm btn-outline-danger d-none" onclick="deleteCurrentGeometry()" title="{% trans 'Delete this geometry point' %}">
27+
<i class="bi bi-trash"></i> {% trans "Delete" %}
28+
</button>
29+
{% endif %}
30+
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSelection()" title="{% trans 'Clear Selection' %}">
31+
<i class="bi bi-x-circle"></i> {% trans "Close" %}
32+
</button>
33+
</div>
2734
</div>
2835

2936
<div id="entriesSection">
@@ -205,6 +212,7 @@ <h5 class="modal-title" id="virtualUserNameModalLabel">{% trans "Welcome" %}</h5
205212
console.log('window.allFields initialized:', window.allFields);
206213
window.allowMultipleEntries = {% if allow_multiple_entries %}true{% else %}false{% endif %};
207214
window.datasetId = {{ dataset.id }};
215+
window.currentUserId = {% if user.is_authenticated %}{{ user.id }}{% else %}null{% endif %};
208216
window.isDatasetOwner = {% if user.is_authenticated %}{% if dataset.owner == user or user.is_superuser %}true{% else %}false{% endif %}{% else %}false{% endif %};
209217
{# Only owner/superuser should load or draw mapping-area polygons; collaborators keep the flag false. #}
210218
window.enableMappingAreas = {% if enable_mapping_areas and user.is_authenticated %}{% if dataset.owner == user or user.is_superuser %}true{% else %}false{% endif %}{% else %}false{% endif %};
@@ -226,7 +234,9 @@ <h5 class="modal-title" id="virtualUserNameModalLabel">{% trans "Welcome" %}</h5
226234
'createEntry': '{% trans "Create New Entry" %}',
227235
'saveEntries': '{% trans "Save Changes" %}',
228236
'geolocationNotSupported': '{% trans "Geolocation is not supported by this browser." %}',
229-
'geolocationError': '{% trans "Error getting your location: " %}'
237+
'geolocationError': '{% trans "Error getting your location: " %}',
238+
'deleteGeometryConfirm': '{% trans "Delete this geometry point and all its entries? This cannot be undone." %}',
239+
'deleteGeometryFailed': '{% trans "Could not delete geometry." %}'
230240
};
231241
</script>
232242
{% endblock %}

0 commit comments

Comments
 (0)