From 5a1aecdde3382e028499b9426cb156524fb94fa8 Mon Sep 17 00:00:00 2001 From: Dimas Ciputra Date: Thu, 28 Dec 2023 09:57:09 +0700 Subject: [PATCH] Feature delete records by source reference id (#3649) * Add button * Feature to delete records by source reference id --- bims/api_urls.py | 8 +- bims/api_views/reference.py | 68 +++++++++++ bims/models/decision_support_tool.py | 5 +- bims/models/survey.py | 2 +- bims/static/css/source_reference_list.css | 59 ++++++++++ .../js/non_requirejs/source_reference_list.js | 76 +++++++++++++ bims/templates/source_reference_list.html | 107 ++++-------------- bims/tests/test_source_reference.py | 92 ++++++++++++++- 8 files changed, 323 insertions(+), 94 deletions(-) create mode 100644 bims/api_views/reference.py create mode 100644 bims/static/css/source_reference_list.css create mode 100644 bims/static/js/non_requirejs/source_reference_list.js diff --git a/bims/api_urls.py b/bims/api_urls.py index d8a629c50..030850f30 100644 --- a/bims/api_urls.py +++ b/bims/api_urls.py @@ -1,4 +1,6 @@ from django.urls import re_path, include, path + +from bims.api_views.reference import DeleteRecordsByReferenceId # from rest_framework.documentation import include_docs_urls from bims.api_views.boundary import ( BoundaryList, @@ -320,8 +322,10 @@ ), re_path(r'^gbif-ids/download/$', GbifIdsDownloader.as_view()), - path('download-layer-data///', DownloadLayerData.as_view(), - name='download-layer-data') + name='download-layer-data'), + path('delete-records-by-source-reference-id//', + DeleteRecordsByReferenceId.as_view(), + name='delete-records-by-source-reference-id') ] diff --git a/bims/api_views/reference.py b/bims/api_views/reference.py new file mode 100644 index 000000000..3b2190f60 --- /dev/null +++ b/bims/api_views/reference.py @@ -0,0 +1,68 @@ +from django.http import ( + Http404, HttpResponseServerError, HttpResponseForbidden +) +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from bims.models.source_reference import ( + SourceReference +) +from bims.models.biological_collection_record import ( + BiologicalCollectionRecord +) +from bims.models.chemical_record import ( + ChemicalRecord +) +from bims.models.decision_support_tool import DecisionSupportTool + + +class DeleteRecordsByReferenceId(APIView): + """ + API endpoint for deleting BiologicalCollectionRecord and ChemicalRecord + instances associated with a given SourceReference ID. + """ + + def post(self, request, *args, **kwargs): + if not request.user.is_superuser: + return HttpResponseForbidden( + 'Only superusers are allowed to perform this action.' + ) + + source_reference_id = kwargs.get('source_reference_id') + if not source_reference_id: + raise Http404('Missing id') + + try: + source_reference = get_object_or_404( + SourceReference, + pk=source_reference_id + ) + messages = [] + bio_records = BiologicalCollectionRecord.objects.filter(source_reference_id=source_reference_id) + if bio_records.exists(): + DecisionSupportTool.objects.filter( + biological_collection_record__id__in=list(bio_records.values_list('id', flat=True)) + ).delete() + BiologicalCollectionRecord.objects.filter(source_reference_id=source_reference_id).delete() + messages.append( + 'BiologicalCollectionRecord successfully deleted' + ) + else: + messages.append("No BiologicalCollectionRecord found for the given reference ID.") + + if ChemicalRecord.objects.filter(source_reference_id=source_reference_id).exists(): + ChemicalRecord.objects.filter(source_reference_id=source_reference_id).delete() + messages.append( + 'ChemicalRecord successfully deleted' + ) + else: + messages.append("No ChemicalRecord found for the given reference ID.") + + return Response( + {'message': messages}, + status=status.HTTP_200_OK) + + except Exception as e: + # In case of any other error, return a 500 Internal Server Error + return HttpResponseServerError(f'An error occurred: {e}') diff --git a/bims/models/decision_support_tool.py b/bims/models/decision_support_tool.py index e7714356c..0b9427cf5 100644 --- a/bims/models/decision_support_tool.py +++ b/bims/models/decision_support_tool.py @@ -29,4 +29,7 @@ class DecisionSupportTool(models.Model): ) def __str__(self): - return f'{self.name} - {self.biological_collection_record.uuid}' + try: + return f'{self.name} - {self.biological_collection_record.uuid}' + except Exception as e: + return f'{self.name}' diff --git a/bims/models/survey.py b/bims/models/survey.py index 27a7a5a6a..d08610153 100644 --- a/bims/models/survey.py +++ b/bims/models/survey.py @@ -5,7 +5,7 @@ import uuid from django.db import models -from django.db.models.signals import post_delete, pre_delete +from django.db.models.signals import pre_delete from django.dispatch import receiver from django.utils import timezone from bims.models import LocationSite diff --git a/bims/static/css/source_reference_list.css b/bims/static/css/source_reference_list.css new file mode 100644 index 000000000..4ddca9b87 --- /dev/null +++ b/bims/static/css/source_reference_list.css @@ -0,0 +1,59 @@ +.card { + margin-bottom: 10px; +} + +.custom-control-label { + max-width: 80%; +} + +.reference-filter-title { + margin-bottom: 0 !important; + font-weight: bold; +} +.badge-occurrences { + font-size: 9pt; + padding-left: 10px; + padding-right: 10px; + padding-top: 8px; + padding-bottom: 8px; +} +.badge-zero { + background-color: #b8b8b8; +} +.reference-type-label { + text-transform: uppercase; + font-weight: 300; + font-size: 10pt; +} +.active.active:hover { + color: white; +} +.input-group-append:hover { + cursor: pointer; +} +.loading-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.loader { + border: 5px solid #f3f3f3; + border-top: 5px solid #3498db; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/bims/static/js/non_requirejs/source_reference_list.js b/bims/static/js/non_requirejs/source_reference_list.js new file mode 100644 index 000000000..b82caa8fa --- /dev/null +++ b/bims/static/js/non_requirejs/source_reference_list.js @@ -0,0 +1,76 @@ +$('.copy-document-id').click(function (e) { + let documentId = window.location.origin + '/documents/' + $(e.target).data('document-id'); + /* Copy the text inside the text field */ + navigator.clipboard.writeText(documentId); + /* Alert the copied text */ + alert("Copied the text: " + documentId); +}) + +$('.custom-control-input').change(function (e) { + let $parent = $(e.target).parent().parent(); + let filterName = $parent.data('filter'); + let checkedVals = $parent.find(':checkbox:checked').map(function () { + return this.value; + }).get(); + insertParam(filterName, checkedVals); +}) + +$('.input-group-append').click(function (e) { + let value = $('#search-input').val(); + insertParam('q', value); +}) + +$('.delete-reference').click(function (e) { + e.preventDefault(); + const referenceId = $(e.target).data('reference-id'); + let r = confirm("Are you sure you want to remove this reference?"); + if (r === true) { + $.ajax({ + url: "/delete-source-reference/", + headers: {"X-CSRFToken": csrfToken}, + type: 'POST', + data: { + 'reference_id': referenceId + }, + success: function (res) { + location.reload(); + } + }); + } +}) + +$('.delete-records').click(function (e) { + e.preventDefault(); + const referenceId = $(e.target).data('reference-id'); + const totalRecords = $(e.target).data('total-records'); + const totalPhysico = $(e.target).data('total-physico-chemical-data'); + let r = confirm( + `Are you sure you want to remove ${totalRecords} records and ${totalPhysico} + Physico-chemical data associated with this source reference?`); + if (r) { + document.getElementById('loading-animation').style.display = 'flex'; + console.log('called'); + $.ajax({ + url: `/api/delete-records-by-source-reference-id/${referenceId}/`, + type: 'POST', + headers: {"X-CSRFToken": csrfToken}, + success: function(response) { + window.location.reload(); + }, + error: function(xhr, textStatus, errorThrown) { + alert('Error: ' + errorThrown); + document.getElementById('loading-animation').style.display = 'none'; + } + }); + } + +}) + + +$('.apply-author-filter').click(function(e) { + const $target = $(e.target).parent(); + const authorIds = $target.find('.author_result').map(function () { + return $(this).data("author-id"); + }).get(); + insertParam('collectors', authorIds.join(',')) +}) diff --git a/bims/templates/source_reference_list.html b/bims/templates/source_reference_list.html index b3178182c..5c7f43201 100644 --- a/bims/templates/source_reference_list.html +++ b/bims/templates/source_reference_list.html @@ -8,45 +8,16 @@ {% block pre-head %} - + {% endblock %} {% block body_content %} +
+ +
+

Explore Source References {{ page_obj.paginator.count }} Result{% if page_obj.paginator.count > 1 %}s{% endif %}

@@ -150,8 +121,17 @@
{{ source_reference.link_template | safe }}
Update
+ {% if source_reference.occurrences > 0 or source_reference.chemical_records > 0 %} + + {% endif %} {% endif %}
@@ -176,58 +156,11 @@
{{ source_reference.link_template | safe }}
+ {% endblock %} diff --git a/bims/tests/test_source_reference.py b/bims/tests/test_source_reference.py index a84527c0c..a20d06eaf 100644 --- a/bims/tests/test_source_reference.py +++ b/bims/tests/test_source_reference.py @@ -3,19 +3,31 @@ import logging from django.test import TestCase +from django.urls import reverse +from django.db.models.signals import post_save +from rest_framework import status +from rest_framework.test import APIClient from bims.models import BiologicalCollectionRecord -from bims.models.source_reference import SourceReference, \ - merge_source_references, SourceReferenceDatabase, SourceReferenceDocument +from bims.models.source_reference import ( + SourceReference, + merge_source_references, SourceReferenceDatabase, + SourceReferenceDocument, source_reference_post_save_handler, + SourceReferenceBibliography +) from bims.tests.model_factories import ( SourceReferenceF, SourceReferenceBibliographyF, SourceReferenceDatabaseF, SourceReferenceDocumentF, DatabaseRecordF, - BiologicalCollectionRecordF, UserF + BiologicalCollectionRecordF, UserF, + ChemicalRecordF, + LocationSite, ) +from bims.models.location_site import location_site_post_save_handler from geonode.documents.models import Document +from bims.models.chemical_record import ChemicalRecord from td_biblio.tests.model_factories import ( JournalF, EntryF @@ -224,3 +236,77 @@ def test_merge_source_reference(self): id=document.id ).exists() ) + + +class TestRemoveRecordsBySourceReference(TestCase): + + def setUp(self): + post_save.disconnect(receiver=location_site_post_save_handler, sender=LocationSite) + post_save.disconnect(receiver=source_reference_post_save_handler, sender=SourceReferenceBibliography) + + self.superuser = UserF.create(is_superuser=True) + self.user = UserF.create() + + self.client = APIClient() + + self.source_reference = SourceReferenceBibliographyF.create( + source_name="Test Reference") + self.bio_record = BiologicalCollectionRecordF.create( + source_reference=self.source_reference + ) + BiologicalCollectionRecordF.create( + source_reference=self.source_reference + ) + BiologicalCollectionRecordF.create( + source_reference=self.source_reference + ) + BiologicalCollectionRecordF.create( + source_reference=self.source_reference + ) + BiologicalCollectionRecordF.create( + source_reference=self.source_reference + ) + self.chem_record = ChemicalRecordF.create( + source_reference=self.source_reference + ) + + self.url = reverse( + 'delete-records-by-source-reference-id', + kwargs={'source_reference_id': self.source_reference.pk}) + + def tearDown(self): + post_save.connect(receiver=location_site_post_save_handler, sender=LocationSite) + post_save.connect(receiver=source_reference_post_save_handler, sender=SourceReferenceBibliography) + + def test_superuser_access(self): + self.client.force_authenticate(user=self.user) + response = self.client.post(self.url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + self.client.force_authenticate(user=self.superuser) + response = self.client.post(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_functionality(self): + self.client.force_authenticate(user=self.superuser) + response = self.client.post(self.url) + self.assertFalse(BiologicalCollectionRecord.objects.filter(source_reference=self.source_reference).exists()) + self.assertFalse(ChemicalRecord.objects.filter(source_reference=self.source_reference).exists()) + + def test_no_records_found(self): + source = SourceReferenceBibliography.objects.create(source_name="Another Reference") + another_url = reverse('delete-records-by-source-reference-id', + kwargs={'source_reference_id': source.id}) + self.client.force_authenticate(user=self.superuser) + response = self.client.post(another_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('No BiologicalCollectionRecord found for the given reference ID.', response.data['message']) + self.assertIn('No ChemicalRecord found for the given reference ID.', response.data['message']) + + def test_missing_or_invalid_id(self): + # Test the response when source_reference_id is missing or invalid + self.client.force_authenticate(user=self.superuser) + invalid_url = reverse('delete-records-by-source-reference-id', kwargs={'source_reference_id': 0}) + response = self.client.post(invalid_url) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) +