[Fixes #296] Delete observation button for admin when no data
parent
560411c3d5
commit
b62b5dd75d
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from shortuuidfield import ShortUUIDField
|
from shortuuidfield import ShortUUIDField
|
||||||
|
|
||||||
|
@ -220,10 +221,15 @@ class Observation(models.Model):
|
||||||
return self.end > now()
|
return self.end > now()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_deletable(self):
|
def is_deletable_before_start(self):
|
||||||
deletion = self.start - timedelta(minutes=int(settings.OBSERVATION_MAX_DELETION_RANGE))
|
deletion = self.start - timedelta(minutes=int(settings.OBSERVATION_MAX_DELETION_RANGE))
|
||||||
return deletion > now()
|
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
|
# observation has at least 1 payload submitted, no verification taken into account
|
||||||
@property
|
@property
|
||||||
def has_submitted_data(self):
|
def has_submitted_data(self):
|
||||||
|
@ -281,6 +287,17 @@ class Data(models.Model):
|
||||||
def is_no_data(self):
|
def is_no_data(self):
|
||||||
return self.vetted_status == 'no_data'
|
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:
|
class Meta:
|
||||||
ordering = ['-start', '-end']
|
ordering = ['-start', '-end']
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.timezone import now
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.conf import settings
|
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,
|
from network.base.models import (ANTENNA_BANDS, ANTENNA_TYPES, RIG_TYPES, OBSERVATION_STATUSES,
|
||||||
Rig, Mode, Antenna, Satellite, Tle, Station, Transmitter,
|
Rig, Mode, Antenna, Satellite, Tle, Station, Transmitter,
|
||||||
|
@ -323,8 +324,12 @@ class ObservationViewTest(TestCase):
|
||||||
observation = None
|
observation = None
|
||||||
satellites = []
|
satellites = []
|
||||||
transmitters = []
|
transmitters = []
|
||||||
|
user = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.user = UserFactory()
|
||||||
|
self.user.user_permissions.add(
|
||||||
|
Permission.objects.get(codename='delete_observation'))
|
||||||
for x in xrange(1, 10):
|
for x in xrange(1, 10):
|
||||||
self.satellites.append(SatelliteFactory())
|
self.satellites.append(SatelliteFactory())
|
||||||
for x in xrange(1, 10):
|
for x in xrange(1, 10):
|
||||||
|
@ -335,6 +340,12 @@ class ObservationViewTest(TestCase):
|
||||||
response = self.client.get('/observations/%d/' % self.observation.id)
|
response = self.client.get('/observations/%d/' % self.observation.id)
|
||||||
self.assertContains(response, self.observation.author.username)
|
self.assertContains(response, self.observation.author.username)
|
||||||
self.assertContains(response, self.observation.transmitter.mode.name)
|
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)
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
@ -360,9 +371,25 @@ class ObservationDeleteTest(TestCase):
|
||||||
# observations in progress cannot be deleted
|
# observations in progress cannot be deleted
|
||||||
self.observation.start = datetime.now() + timedelta(
|
self.observation.start = datetime.now() + timedelta(
|
||||||
minutes=(2 * int(settings.OBSERVATION_MAX_DELETION_RANGE)))
|
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()
|
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)
|
response = self.client.get('/observations/%d/delete/' % self.observation.id)
|
||||||
self.assertRedirects(response, '/observations/')
|
self.assertRedirects(response, '/observations/')
|
||||||
response = self.client.get('/observations/')
|
response = self.client.get('/observations/')
|
||||||
|
@ -531,6 +558,8 @@ class ObservationModelTest(TestCase):
|
||||||
observation = None
|
observation = None
|
||||||
satellites = []
|
satellites = []
|
||||||
transmitters = []
|
transmitters = []
|
||||||
|
user = None
|
||||||
|
admin = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for x in xrange(1, 10):
|
for x in xrange(1, 10):
|
||||||
|
@ -547,6 +576,22 @@ class ObservationModelTest(TestCase):
|
||||||
def test_is_passed(self):
|
def test_is_passed(self):
|
||||||
self.assertTrue(self.observation.is_past)
|
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)
|
@pytest.mark.django_db(transaction=True)
|
||||||
class DataModelTest(TestCase):
|
class DataModelTest(TestCase):
|
||||||
|
@ -554,6 +599,7 @@ class DataModelTest(TestCase):
|
||||||
Test various properties of the Observation Model
|
Test various properties of the Observation Model
|
||||||
"""
|
"""
|
||||||
data = None
|
data = None
|
||||||
|
data2 = None
|
||||||
satellites = []
|
satellites = []
|
||||||
transmitters = []
|
transmitters = []
|
||||||
|
|
||||||
|
@ -566,9 +612,13 @@ class DataModelTest(TestCase):
|
||||||
self.data.end = now()
|
self.data.end = now()
|
||||||
self.data.vetted_status = 'no_data'
|
self.data.vetted_status = 'no_data'
|
||||||
self.data.save()
|
self.data.save()
|
||||||
|
self.data2 = DataFactory(payload=None)
|
||||||
|
|
||||||
def test_is_no_data(self):
|
def test_is_no_data(self):
|
||||||
self.assertTrue(self.data.is_no_data)
|
self.assertTrue(self.data.is_no_data)
|
||||||
|
|
||||||
def test_is_passed(self):
|
def test_is_passed(self):
|
||||||
self.assertTrue(self.data.is_past)
|
self.assertTrue(self.data.is_past)
|
||||||
|
|
||||||
|
def test_payload_exists(self):
|
||||||
|
self.assertFalse(self.data.payload_exists)
|
||||||
|
|
|
@ -175,8 +175,6 @@ class ObservationListView(ListView):
|
||||||
# so cannot use queryset filtering
|
# so cannot use queryset filtering
|
||||||
resultset = []
|
resultset = []
|
||||||
for ob in observations:
|
for ob in observations:
|
||||||
if ob.has_unvetted_data > 0:
|
|
||||||
print ob.has_unvetted_data
|
|
||||||
if bad and ob.has_no_data:
|
if bad and ob.has_no_data:
|
||||||
resultset.append(ob)
|
resultset.append(ob)
|
||||||
elif good and ob.has_verified_data:
|
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):
|
def observation_view(request, id):
|
||||||
"""View for single observation page."""
|
"""View for single observation page."""
|
||||||
observation = get_object_or_404(Observation, id=id)
|
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
|
# not all users will be able to vet data within an observation, allow
|
||||||
# staff, observation requestors, and station owners
|
# staff, observation requestors, and station owners
|
||||||
is_vetting_user = False
|
is_vetting_user = False
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
if request.user == observation.author or \
|
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:
|
request.user.is_staff:
|
||||||
is_vetting_user = True
|
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':
|
if settings.ENVIRONMENT == 'production':
|
||||||
discuss_slug = 'https://community.satnogs.org/t/observation-{0}-{1}-{2}' \
|
discuss_slug = 'https://community.satnogs.org/t/observation-{0}-{1}-{2}' \
|
||||||
.format(observation.id, slugify(observation.satellite.name),
|
.format(observation.id, slugify(observation.satellite.name),
|
||||||
|
@ -413,12 +426,14 @@ def observation_view(request, id):
|
||||||
has_comments = False
|
has_comments = False
|
||||||
|
|
||||||
return render(request, 'base/observation_view.html',
|
return render(request, 'base/observation_view.html',
|
||||||
{'observation': observation, 'data': data, 'has_comments': has_comments,
|
{'observation': observation, 'dataset': dataset,
|
||||||
'discuss_url': discuss_url, 'discuss_slug': discuss_slug,
|
'has_comments': has_comments, 'discuss_url': discuss_url,
|
||||||
'is_vetting_user': is_vetting_user})
|
'discuss_slug': discuss_slug, 'is_vetting_user': is_vetting_user,
|
||||||
|
'is_deletable': is_deletable})
|
||||||
|
|
||||||
return render(request, 'base/observation_view.html',
|
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
|
@login_required
|
||||||
|
@ -426,7 +441,14 @@ def observation_delete(request, id):
|
||||||
"""View for deleting observation."""
|
"""View for deleting observation."""
|
||||||
me = request.user
|
me = request.user
|
||||||
observation = get_object_or_404(Observation, id=id)
|
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()
|
observation.delete()
|
||||||
messages.success(request, 'Observation deleted successfully.')
|
messages.success(request, 'Observation deleted successfully.')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -220,6 +220,7 @@ DATE_MAX_RANGE = '480'
|
||||||
# Station heartbeat in minutes
|
# Station heartbeat in minutes
|
||||||
STATION_HEARTBEAT_TIME = getenv('STATION_HEARTBEAT_TIME', 60)
|
STATION_HEARTBEAT_TIME = getenv('STATION_HEARTBEAT_TIME', 60)
|
||||||
OBSERVATION_MAX_DELETION_RANGE = getenv('OBSERVATION_MAX_DELETION_RANGE', 10)
|
OBSERVATION_MAX_DELETION_RANGE = getenv('OBSERVATION_MAX_DELETION_RANGE', 10)
|
||||||
|
OBSERVATION_MIN_DELETION_RANGE = getenv('OBSERVATION_MIN_DELETION_RANGE', 60)
|
||||||
|
|
||||||
# DB API
|
# DB API
|
||||||
DB_API_ENDPOINT = getenv('DB_API_ENDPOINT', 'https://db.satnogs.org/api/')
|
DB_API_ENDPOINT = getenv('DB_API_ENDPOINT', 'https://db.satnogs.org/api/')
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-right">
|
<div class="col-md-6 text-right">
|
||||||
<h2>
|
<h2>
|
||||||
{% if observation.author == request.user and observation.is_deletable %}
|
{% if is_deletable %}
|
||||||
<a href="{% url 'base:observation_delete' id=observation.id %}" id="obs-delete" class="btn btn-danger">Delete Observation</a>
|
<a href="{% url 'base:observation_delete' id=observation.id %}" id="obs-delete" class="btn btn-danger">Delete Observation</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for data in data %}
|
{% for data in dataset %}
|
||||||
<div class="panel panel-default observation-data" id="{{ data.id }}"
|
<div class="panel panel-default observation-data" id="{{ data.id }}"
|
||||||
data-start="{{ data.start|date:"U" }}"
|
data-start="{{ data.start|date:"U" }}"
|
||||||
data-end="{{ data.end|date:"U" }}"
|
data-end="{{ data.end|date:"U" }}"
|
||||||
|
@ -186,7 +186,6 @@
|
||||||
<span class="glyphicon glyphicon-picture"></span> Waterfall
|
<span class="glyphicon glyphicon-picture"></span> Waterfall
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="pull-right hidden-xs">
|
<span class="pull-right hidden-xs">
|
||||||
<span class="label label-default">Timeframe</span>
|
<span class="label label-default">Timeframe</span>
|
||||||
{{ data.start|date:"Y-m-d H:i:s" }} - {{ data.end|date:"Y-m-d H:i:s" }}
|
{{ data.start|date:"Y-m-d H:i:s" }} - {{ data.end|date:"Y-m-d H:i:s" }}
|
||||||
|
|
Loading…
Reference in New Issue