Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Observation Deletion Functionality. #746

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -455,3 +457,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 @@ -693,3 +695,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)
Loading