Skip to content

Commit

Permalink
Add observation deletion functionality.
Browse files Browse the repository at this point in the history
- Implement unified method for deleting observations and associated data products.
- Resolve bug preventing deletion of thumbnails.
- Introduce new template for observation deletion process.
  • Loading branch information
davner committed Nov 16, 2023
1 parent bbdef40 commit 4428d9b
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 9 deletions.
35 changes: 35 additions & 0 deletions tom_common/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Union

from tom_dataproducts.models import DataProduct, ReducedDatum
from tom_observations.models import ObservationRecord


def delete_associated_data_products(record_or_product: Union[ObservationRecord, DataProduct]) -> None:
"""
Utility function to delete associated DataProducts or a single DataProduct.
Parameters
----------
record_or_product : Union[ObservationRecord, DataProduct]
The ObservationRecord object to find associated DataProducts,
or a single DataProduct to be deleted.
"""
if isinstance(record_or_product, ObservationRecord):
query = DataProduct.objects.filter(observation_record=record_or_product)
elif isinstance(record_or_product, DataProduct):
query = [record_or_product]
else:
raise ValueError("Invalid argument type. Must be ObservationRecord or DataProduct.")

for data_product in query:
# Delete associated ReducedDatum objects.
ReducedDatum.objects.filter(data_product=data_product).delete()

# Delete the file from the disk.
data_product.data.delete()

# Delete thumbnail from the disk.
data_product.thumbnail.delete()

# Delete the `DataProduct` object from the database.
data_product.delete()
10 changes: 2 additions & 8 deletions tom_dataproducts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from tom_common.hooks import run_hook
from tom_common.hints import add_hint
from tom_common.mixins import Raise403PermissionRequiredMixin
from tom_common.utils import delete_associated_data_products
from tom_dataproducts.models import DataProduct, DataProductGroup, ReducedDatum
from tom_dataproducts.exceptions import InvalidFileFormatException
from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm, DataShareForm
Expand Down Expand Up @@ -279,14 +280,7 @@ def form_valid(self, form):
"""
# Fetch the DataProduct object
data_product = self.get_object()

# Delete associated ReducedDatum objects
ReducedDatum.objects.filter(data_product=data_product).delete()

# Delete the file reference.
data_product.data.delete()
# Delete the `DataProduct` object from the database.
data_product.delete()
delete_associated_data_products(data_product)

return HttpResponseRedirect(self.get_success_url())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends 'tom_common/base.html' %}
{% load bootstrap4 %}
{% block title %}Confirm delete{% endblock %}
{% block content %}
<h3>Confirm Delete</h3>
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete observation ID {{ object.observation_id }} for target {{ object }}"?</p>
{% buttons %}
<button type="submit" class="btn btn-danger">Confirm</button>
{% endbuttons %}
</form>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ <h2>{{ object }}
<p class="my-auto"><strong>Observation ID:</strong> {{ object.observation_id }}</p>
<p class="my-auto"><strong>Created:</strong> {{ object.created }} <strong>Modified:</strong> {{ object.modified }}</p>
<p class="my-auto"><strong>Status:</strong> {{ object.status }}</p>
<a href="{% url 'tom_observations:delete' object.id %}" title="Delete observation" class="btn btn-danger">Delete</a>
</div>
<p></p>
{% upload_dataproduct object %}
Expand Down
37 changes: 37 additions & 0 deletions tom_observations/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django.contrib.auth.models import User
from django.contrib.messages import get_messages
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import ValidationError
from django.test import TestCase, override_settings
from django.urls import reverse
Expand All @@ -13,6 +14,7 @@
from astropy.time import Time

from .factories import ObservingRecordFactory, ObservationTemplateFactory, SiderealTargetFactory, TargetNameFactory
from tom_dataproducts.models import DataProduct
from tom_observations.utils import get_astroplan_sun_and_time, get_sidereal_visibility
from tom_observations.tests.utils import FakeRoboticFacility
from tom_observations.models import ObservationRecord, ObservationGroup, ObservationTemplate
Expand Down Expand Up @@ -436,3 +438,38 @@ def test_get_visibility_sidereal(self, mock_facility):
self.assertEqual(len(airmass_data), len(expected_airmass))
for i, expected_airmass_value in enumerate(expected_airmass):
self.assertAlmostEqual(airmass_data[i], expected_airmass_value, places=3)


@override_settings(TOM_FACILITY_CLASSES=['tom_observations.tests.utils.FakeRoboticFacility'])
class TestObservationRecordDeleteView(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpassword')
self.client.force_login(self.user)
self.target = SiderealTargetFactory.create()
self.target_name = TargetNameFactory.create(target=self.target)
self.observation_record = ObservingRecordFactory.create(
target_id=self.target.id,
facility=FakeRoboticFacility.name,
parameters={}
)
self.data_product = DataProduct.objects.create(
product_id='testproductid',
target=self.target,
observation_record=self.observation_record,
data=SimpleUploadedFile('afile.fits', b'somedata')
)
assign_perm('tom_observations.delete_observationrecord', self.user, self.observation_record)
assign_perm('tom_targets.delete_dataproduct', self.user, self.data_product)

def test_delete_observation(self):
self.assertTrue(ObservationRecord.objects.filter(id=self.observation_record.id).exists())
self.assertTrue(DataProduct.objects.filter(product_id='testproductid').exists())
delete_url = reverse('tom_observations:delete', kwargs={'pk': self.observation_record.id})
response = self.client.post(delete_url, follow=True)

# Check if the response redirected to the success URL (observations list).
self.assertRedirects(response, reverse("tom_observations:list"))

# Verify that the ObservationRecord and DataProduct no longer exists.
self.assertFalse(ObservationRecord.objects.filter(id=self.observation_record.id).exists())
self.assertFalse(DataProduct.objects.filter(product_id='testproductid').exists())
3 changes: 2 additions & 1 deletion tom_observations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
ObservationGroupListView, ObservationListView, ObservationRecordCancelView,
ObservationRecordDetailView, ObservationTemplateCreateView,
ObservationTemplateDeleteView, ObservationTemplateListView,
ObservationTemplateUpdateView)
ObservationTemplateUpdateView, ObservationRecordDeleteView)
from tom_observations.api_views import ObservationRecordViewSet
from tom_common.api_router import SharedAPIRootRouter

Expand All @@ -16,6 +16,7 @@

urlpatterns = [
path('add/', AddExistingObservationView.as_view(), name='add-existing'),
path('<int:pk>/delete', ObservationRecordDeleteView.as_view(), name='delete'),
path('list/', ObservationListView.as_view(), name='list'),
path('status/', FacilityStatusView.as_view(), name='facility-status'),
path('template/list/', ObservationTemplateListView.as_view(), name='template-list'),
Expand Down
29 changes: 29 additions & 0 deletions tom_observations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django_filters import (CharFilter, ChoiceFilter, DateTimeFromToRangeFilter, FilterSet, ModelMultipleChoiceFilter,
OrderingFilter)
from django_filters.views import FilterView
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.safestring import mark_safe
Expand All @@ -26,6 +27,7 @@

from tom_common.hints import add_hint
from tom_common.mixins import Raise403PermissionRequiredMixin
from tom_common.utils import delete_associated_data_products
from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm
from tom_dataproducts.models import is_fits_image_file
from tom_observations.cadence import CadenceForm, get_cadence_strategy
Expand Down Expand Up @@ -667,3 +669,30 @@ class ObservationTemplateDeleteView(LoginRequiredMixin, DeleteView):

class FacilityStatusView(TemplateView):
template_name = 'tom_observations/facility_status.html'


class ObservationRecordDeleteView(Raise403PermissionRequiredMixin, DeleteView):
"""View for deleting an observation."""
permission_required = "tom_observations.delete_observationrecord"
success_url = reverse_lazy("observations:list")
model = ObservationRecord

def form_valid(self, form: forms.Form) -> HttpResponse:
"""Handle deletion of associated DataProducts upon valid form
submission.
Parameters
----------
form : `Form`
The form object.
Returns
-------
`HttpResponse`
HTTP response indicating the outcome of the deletion process.
"""
# Fetch the ObservationRecord object.
observation_record = self.get_object()
delete_associated_data_products(observation_record)

return super().form_valid(form)

0 comments on commit 4428d9b

Please sign in to comment.