diff --git a/network/base/models.py b/network/base/models.py index 2b00d82..3d46bb3 100644 --- a/network/base/models.py +++ b/network/base/models.py @@ -1,3 +1,4 @@ +import os from datetime import datetime, timedelta from shortuuidfield import ShortUUIDField @@ -220,10 +221,15 @@ class Observation(models.Model): return self.end > now() @property - def is_deletable(self): + def is_deletable_before_start(self): deletion = self.start - timedelta(minutes=int(settings.OBSERVATION_MAX_DELETION_RANGE)) return deletion > now() + @property + def is_deletable_after_end(self): + deletion = self.end + timedelta(minutes=int(settings.OBSERVATION_MIN_DELETION_RANGE)) + return deletion < now() + # observation has at least 1 payload submitted, no verification taken into account @property def has_submitted_data(self): @@ -281,6 +287,17 @@ class Data(models.Model): def is_no_data(self): return self.vetted_status == 'no_data' + @property + def payload_exists(self): + """ Run some checks on the payload for existence of data """ + if self.payload is None: + return False + if not os.path.isfile(os.path.join(settings.MEDIA_ROOT, self.payload.name)): + return False + if self.payload.size == 0: + return False + return True + class Meta: ordering = ['-start', '-end'] diff --git a/network/base/tests.py b/network/base/tests.py index 61a2ee5..2fd9c75 100644 --- a/network/base/tests.py +++ b/network/base/tests.py @@ -10,6 +10,7 @@ from django.utils.timezone import now from django.db import transaction from django.test import TestCase, Client from django.conf import settings +from django.contrib.auth.models import Permission from network.base.models import (ANTENNA_BANDS, ANTENNA_TYPES, RIG_TYPES, OBSERVATION_STATUSES, Rig, Mode, Antenna, Satellite, Tle, Station, Transmitter, @@ -329,8 +330,12 @@ class ObservationViewTest(TestCase): observation = None satellites = [] transmitters = [] + user = None def setUp(self): + self.user = UserFactory() + self.user.user_permissions.add( + Permission.objects.get(codename='delete_observation')) for x in xrange(1, 10): self.satellites.append(SatelliteFactory()) for x in xrange(1, 10): @@ -341,6 +346,12 @@ class ObservationViewTest(TestCase): response = self.client.get('/observations/%d/' % self.observation.id) self.assertContains(response, self.observation.author.username) self.assertContains(response, self.observation.transmitter.mode.name) + self.assertNotContains(response, 'Delete Observation') + + def test_observation_staff(self): + self.client.force_login(self.user) + response = self.client.get('/observations/%d/' % self.observation.id) + self.assertContains(response, 'Delete Observation') @pytest.mark.django_db(transaction=True) @@ -366,9 +377,25 @@ class ObservationDeleteTest(TestCase): # observations in progress cannot be deleted self.observation.start = datetime.now() + timedelta( minutes=(2 * int(settings.OBSERVATION_MAX_DELETION_RANGE))) + self.observation.end = datetime.now() - timedelta( + minutes=(2 * int(settings.OBSERVATION_MIN_DELETION_RANGE))) self.observation.save() - def test_observation_delete(self): + def test_observation_delete_author(self): + """Deletion OK when user is the author of the observation""" + response = self.client.get('/observations/%d/delete/' % self.observation.id) + self.assertRedirects(response, '/observations/') + response = self.client.get('/observations/') + with self.assertRaises(Observation.DoesNotExist): + _lookup = Observation.objects.get(pk=self.observation.id) # noqa:F841 + + def test_observation_delete_staff(self): + """Deletion OK when user is staff and there is no data""" + self.user = UserFactory() + self.user.user_permissions.add( + Permission.objects.get(codename='delete_observation')) + self.user.save() + self.client.force_login(self.user) response = self.client.get('/observations/%d/delete/' % self.observation.id) self.assertRedirects(response, '/observations/') response = self.client.get('/observations/') @@ -537,6 +564,8 @@ class ObservationModelTest(TestCase): observation = None satellites = [] transmitters = [] + user = None + admin = None def setUp(self): for x in xrange(1, 10): @@ -553,6 +582,22 @@ class ObservationModelTest(TestCase): def test_is_passed(self): self.assertTrue(self.observation.is_past) + def test_is_deletable_before_start(self): + self.observation.start = now() - timedelta(minutes=2) + self.observation.save() + self.assertFalse(self.observation.is_deletable_before_start) + self.observation.start = now() + timedelta(minutes=100) + self.observation.save() + self.assertTrue(self.observation.is_deletable_before_start) + + def test_is_deletable_after_end(self): + self.observation.end = now() + self.observation.save() + self.assertFalse(self.observation.is_deletable_after_end) + self.observation.end = now() - timedelta(minutes=200) + self.observation.save() + self.assertTrue(self.observation.is_deletable_after_end) + @pytest.mark.django_db(transaction=True) class DataModelTest(TestCase): @@ -560,6 +605,7 @@ class DataModelTest(TestCase): Test various properties of the Observation Model """ data = None + data2 = None satellites = [] transmitters = [] @@ -572,9 +618,13 @@ class DataModelTest(TestCase): self.data.end = now() self.data.vetted_status = 'no_data' self.data.save() + self.data2 = DataFactory(payload=None) def test_is_no_data(self): self.assertTrue(self.data.is_no_data) def test_is_passed(self): self.assertTrue(self.data.is_past) + + def test_payload_exists(self): + self.assertFalse(self.data.payload_exists) diff --git a/network/base/views.py b/network/base/views.py index d474e2a..a5dede6 100644 --- a/network/base/views.py +++ b/network/base/views.py @@ -175,8 +175,6 @@ class ObservationListView(ListView): # so cannot use queryset filtering resultset = [] for ob in observations: - if ob.has_unvetted_data > 0: - print ob.has_unvetted_data if bad and ob.has_no_data: resultset.append(ob) elif good and ob.has_verified_data: @@ -384,17 +382,32 @@ def prediction_windows(request, sat_id, start_date, end_date, station_id=None): def observation_view(request, id): """View for single observation page.""" observation = get_object_or_404(Observation, id=id) - data = Data.objects.filter(observation=observation) + dataset = Data.objects.filter(observation=observation) # not all users will be able to vet data within an observation, allow # staff, observation requestors, and station owners is_vetting_user = False if request.user.is_authenticated(): if request.user == observation.author or \ - data.filter(ground_station__in=Station.objects.filter(owner=request.user)).count or \ + dataset.filter( + ground_station__in=Station.objects.filter(owner=request.user)).count or \ request.user.is_staff: is_vetting_user = True + # Determine if there is no valid payload file in the observation dataset + if request.user.has_perm('base.delete_observation'): + data_payload_exists = False + for data in dataset: + if data.payload_exists: + data_payload_exists = True + # This context flag will determine if a delete button appears for the observation. + is_deletable = False + if observation.author == request.user and observation.is_deletable_before_start: + is_deletable = True + if request.user.has_perm('base.delete_observation') and not data_payload_exists and \ + observation.is_deletable_after_end: + is_deletable = True + if settings.ENVIRONMENT == 'production': discuss_slug = 'https://community.satnogs.org/t/observation-{0}-{1}-{2}' \ .format(observation.id, slugify(observation.satellite.name), @@ -413,12 +426,14 @@ def observation_view(request, id): has_comments = False return render(request, 'base/observation_view.html', - {'observation': observation, 'data': data, 'has_comments': has_comments, - 'discuss_url': discuss_url, 'discuss_slug': discuss_slug, - 'is_vetting_user': is_vetting_user}) + {'observation': observation, 'dataset': dataset, + 'has_comments': has_comments, 'discuss_url': discuss_url, + 'discuss_slug': discuss_slug, 'is_vetting_user': is_vetting_user, + 'is_deletable': is_deletable}) return render(request, 'base/observation_view.html', - {'observation': observation, 'data': data, 'is_vetting_user': is_vetting_user}) + {'observation': observation, 'dataset': dataset, + 'is_vetting_user': is_vetting_user, 'is_deletable': is_deletable}) @login_required @@ -426,7 +441,14 @@ def observation_delete(request, id): """View for deleting observation.""" me = request.user observation = get_object_or_404(Observation, id=id) - if observation.author == me and observation.is_deletable: + # Having non-existent data is also grounds for deletion if user is staff + data_payload_exists = False + for data in observation.data_set.all(): + if data.payload_exists: + data_payload_exists = True + if (observation.author == me and observation.is_deletable_before_start) or \ + (request.user.has_perm('base.delete_observation') and + not data_payload_exists and observation.is_deletable_after_end): observation.delete() messages.success(request, 'Observation deleted successfully.') else: diff --git a/network/settings/base.py b/network/settings/base.py index 6c81af0..b2965ad 100644 --- a/network/settings/base.py +++ b/network/settings/base.py @@ -220,6 +220,7 @@ DATE_MAX_RANGE = '480' # Station heartbeat in minutes STATION_HEARTBEAT_TIME = getenv('STATION_HEARTBEAT_TIME', 60) OBSERVATION_MAX_DELETION_RANGE = getenv('OBSERVATION_MAX_DELETION_RANGE', 10) +OBSERVATION_MIN_DELETION_RANGE = getenv('OBSERVATION_MIN_DELETION_RANGE', 60) # DB API DB_API_ENDPOINT = getenv('DB_API_ENDPOINT', 'https://db.satnogs.org/api/') diff --git a/network/templates/base/observation_view.html b/network/templates/base/observation_view.html index bd2eb6d..6766cb9 100644 --- a/network/templates/base/observation_view.html +++ b/network/templates/base/observation_view.html @@ -20,7 +20,7 @@

- {% if observation.author == request.user and observation.is_deletable %} + {% if is_deletable %} Delete Observation {% endif %}

@@ -101,7 +101,7 @@
- {% for data in data %} + {% for data in dataset %}
{% endif %} -