2019-09-23 05:08:11 -06:00
|
|
|
import logging
|
2017-02-09 05:33:44 -07:00
|
|
|
import os
|
2019-07-15 03:07:17 -06:00
|
|
|
from datetime import timedelta
|
2015-05-06 02:53:48 -06:00
|
|
|
|
2019-09-23 05:08:11 -06:00
|
|
|
import requests
|
2017-08-22 13:07:23 -06:00
|
|
|
from django.conf import settings
|
2018-03-07 11:59:51 -07:00
|
|
|
from django.core.cache import cache
|
2018-08-14 07:38:13 -06:00
|
|
|
from django.core.exceptions import ValidationError
|
2019-09-23 05:08:11 -06:00
|
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
2014-09-01 14:21:53 -06:00
|
|
|
from django.db import models
|
2019-07-24 03:34:15 -06:00
|
|
|
from django.db.models import OuterRef, Subquery
|
2017-12-27 06:15:02 -07:00
|
|
|
from django.db.models.signals import post_save
|
2019-09-23 05:08:11 -06:00
|
|
|
from django.dispatch import receiver
|
2017-09-11 04:33:49 -06:00
|
|
|
from django.urls import reverse
|
2015-05-06 04:06:19 -06:00
|
|
|
from django.utils.html import format_html
|
2017-08-22 13:07:23 -06:00
|
|
|
from django.utils.timezone import now
|
2019-09-23 05:08:11 -06:00
|
|
|
from PIL import Image
|
|
|
|
from rest_framework.authtoken.models import Token
|
|
|
|
from shortuuidfield import ShortUUIDField
|
2014-09-01 14:21:53 -06:00
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
from network.base.managers import ObservationManager
|
2019-09-23 05:08:11 -06:00
|
|
|
from network.users.models import User
|
2014-09-01 14:21:53 -06:00
|
|
|
|
|
|
|
ANTENNA_BANDS = ['HF', 'VHF', 'UHF', 'L', 'S', 'C', 'X', 'KU']
|
|
|
|
ANTENNA_TYPES = (
|
|
|
|
('dipole', 'Dipole'),
|
2018-08-12 03:16:37 -06:00
|
|
|
('v-dipole', 'V-Dipole'),
|
2018-08-17 07:52:08 -06:00
|
|
|
('discone', 'Discone'),
|
2018-08-22 13:48:10 -06:00
|
|
|
('ground', 'Ground Plane'),
|
2014-09-01 14:21:53 -06:00
|
|
|
('yagi', 'Yagi'),
|
2018-09-15 09:04:01 -06:00
|
|
|
('cross-yagi', 'Cross Yagi'),
|
2014-09-01 14:21:53 -06:00
|
|
|
('helical', 'Helical'),
|
|
|
|
('parabolic', 'Parabolic'),
|
2019-03-06 07:05:42 -07:00
|
|
|
('vertical', 'Vertical'),
|
2017-05-19 13:44:22 -06:00
|
|
|
('turnstile', 'Turnstile'),
|
|
|
|
('quadrafilar', 'Quadrafilar'),
|
|
|
|
('eggbeater', 'Eggbeater'),
|
|
|
|
('lindenblad', 'Lindenblad'),
|
2019-03-06 07:05:42 -07:00
|
|
|
('paralindy', 'Parasitic Lindenblad'),
|
|
|
|
('patch', 'Patch')
|
2014-09-01 14:21:53 -06:00
|
|
|
)
|
Initial data vetting/verification system
Model change (with migration 0006) adds 3 fields to Data:
vetted_status (charfield with options for data status, default "unknown")
vetted_user (who vetted the data)
vetted_datetime (when it was vetted)
In addition, various boolean functions are added for the Data model
to check statuses. More functions are added to the Observation model
to check status of verification within an observation as well, assuming
multiple data entries in an Observation. With these, I also changed
"has_data" to "has_submitted_data" to be more specific alongside the
others.
For UX, we add a green check sign or red removal sign to the data
header in Observation view (along with green/red datetime in the footer)
if a data is verified good or bad, respectively. If there is an unknown
status, the data header is given a thumbs-up and thumbs-down button to
verify the data good or bad. These icons are only offered to is_staff,
the observation requestor, and any station owner in the observation.
These buttons trigger new URLs/functions in view:
data_verify(id)
data_mark_bad(id)
Returning the user back to the originating Observation page.
In the observation lists I changed the coloring of the ID button to be:
Future: light blue (same)
No uploaded data and/or all vetted bad data: red
Some or all unvetted data with no verified good data: orange
Some or all verified good data: green
These changes are reflected in the observations.html, home.html, and
user_detail.html templates.
solves satnogs/satnogs-network#171
2016-03-25 13:52:45 -06:00
|
|
|
OBSERVATION_STATUSES = (
|
|
|
|
('unknown', 'Unknown'),
|
2017-12-24 12:17:37 -07:00
|
|
|
('good', 'Good'),
|
|
|
|
('bad', 'Bad'),
|
|
|
|
('failed', 'Failed'),
|
Initial data vetting/verification system
Model change (with migration 0006) adds 3 fields to Data:
vetted_status (charfield with options for data status, default "unknown")
vetted_user (who vetted the data)
vetted_datetime (when it was vetted)
In addition, various boolean functions are added for the Data model
to check statuses. More functions are added to the Observation model
to check status of verification within an observation as well, assuming
multiple data entries in an Observation. With these, I also changed
"has_data" to "has_submitted_data" to be more specific alongside the
others.
For UX, we add a green check sign or red removal sign to the data
header in Observation view (along with green/red datetime in the footer)
if a data is verified good or bad, respectively. If there is an unknown
status, the data header is given a thumbs-up and thumbs-down button to
verify the data good or bad. These icons are only offered to is_staff,
the observation requestor, and any station owner in the observation.
These buttons trigger new URLs/functions in view:
data_verify(id)
data_mark_bad(id)
Returning the user back to the originating Observation page.
In the observation lists I changed the coloring of the ID button to be:
Future: light blue (same)
No uploaded data and/or all vetted bad data: red
Some or all unvetted data with no verified good data: orange
Some or all verified good data: green
These changes are reflected in the observations.html, home.html, and
user_detail.html templates.
solves satnogs/satnogs-network#171
2016-03-25 13:52:45 -06:00
|
|
|
)
|
2017-11-17 07:31:54 -07:00
|
|
|
STATION_STATUSES = (
|
|
|
|
(2, 'Online'),
|
|
|
|
(1, 'Testing'),
|
|
|
|
(0, 'Offline'),
|
|
|
|
)
|
2017-08-12 10:03:51 -06:00
|
|
|
SATELLITE_STATUS = ['alive', 'dead', 're-entered']
|
2019-05-02 14:05:25 -06:00
|
|
|
TRANSMITTER_STATUS = ['active', 'inactive', 'invalid']
|
2018-12-30 14:19:10 -07:00
|
|
|
TRANSMITTER_TYPE = ['Transmitter', 'Transceiver', 'Transponder']
|
2015-08-14 07:55:43 -06:00
|
|
|
|
|
|
|
|
2018-01-14 03:49:18 -07:00
|
|
|
def _name_obs_files(instance, filename):
|
|
|
|
return 'data_obs/{0}/{1}'.format(instance.id, filename)
|
|
|
|
|
|
|
|
|
2018-01-14 09:40:21 -07:00
|
|
|
def _name_obs_demoddata(instance, filename):
|
2019-03-07 12:41:22 -07:00
|
|
|
# On change of the string bellow, change it also at api/views.py
|
2018-01-14 09:40:21 -07:00
|
|
|
return 'data_obs/{0}/{1}'.format(instance.observation.id, filename)
|
|
|
|
|
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
def _observation_post_save(sender, instance, created, **kwargs):
|
|
|
|
"""
|
|
|
|
Post save Observation operations
|
|
|
|
* Auto vet as good obserfvation with Demod Data
|
|
|
|
* Mark Observations from testing stations
|
2018-08-27 06:25:27 -06:00
|
|
|
* Update client version for ground station
|
2017-11-17 07:31:54 -07:00
|
|
|
"""
|
|
|
|
post_save.disconnect(_observation_post_save, sender=Observation)
|
|
|
|
if created and instance.ground_station.testing:
|
|
|
|
instance.testing = True
|
|
|
|
instance.save()
|
2018-08-08 09:33:13 -06:00
|
|
|
if instance.has_demoddata and instance.vetted_status == 'unknown':
|
2017-12-27 06:15:02 -07:00
|
|
|
instance.vetted_status = 'good'
|
|
|
|
instance.vetted_datetime = now()
|
|
|
|
instance.save()
|
2017-11-17 07:31:54 -07:00
|
|
|
post_save.connect(_observation_post_save, sender=Observation)
|
|
|
|
|
|
|
|
|
|
|
|
def _station_post_save(sender, instance, created, **kwargs):
|
|
|
|
"""
|
|
|
|
Post save Station operations
|
|
|
|
* Store current status
|
|
|
|
"""
|
|
|
|
post_save.disconnect(_station_post_save, sender=Station)
|
|
|
|
if not created:
|
2018-03-23 07:28:23 -06:00
|
|
|
current_status = instance.status
|
2017-11-17 07:31:54 -07:00
|
|
|
if instance.is_offline:
|
|
|
|
instance.status = 0
|
|
|
|
elif instance.testing:
|
|
|
|
instance.status = 1
|
|
|
|
else:
|
|
|
|
instance.status = 2
|
|
|
|
instance.save()
|
2018-03-23 07:28:23 -06:00
|
|
|
if instance.status != current_status:
|
|
|
|
StationStatusLog.objects.create(station=instance, status=instance.status)
|
|
|
|
else:
|
|
|
|
StationStatusLog.objects.create(station=instance, status=instance.status)
|
2017-11-17 07:31:54 -07:00
|
|
|
post_save.connect(_station_post_save, sender=Station)
|
2017-12-27 06:15:02 -07:00
|
|
|
|
|
|
|
|
2018-12-05 14:57:39 -07:00
|
|
|
def _tle_post_save(sender, instance, created, **kwargs):
|
|
|
|
"""
|
|
|
|
Post save Tle operations
|
|
|
|
* Update TLE for future observations
|
|
|
|
"""
|
|
|
|
if created:
|
2019-06-18 05:31:13 -06:00
|
|
|
start = now() + timedelta(minutes=10)
|
|
|
|
Observation.objects.filter(satellite=instance.satellite, start__gt=start) \
|
2018-12-05 14:57:39 -07:00
|
|
|
.update(tle=instance.id)
|
|
|
|
|
|
|
|
|
2018-08-14 07:38:13 -06:00
|
|
|
def validate_image(fieldfile_obj):
|
|
|
|
filesize = fieldfile_obj.file.size
|
|
|
|
megabyte_limit = 2.0
|
|
|
|
if filesize > megabyte_limit * 1024 * 1024:
|
|
|
|
raise ValidationError("Max file size is %sMB" % str(megabyte_limit))
|
|
|
|
|
|
|
|
|
2014-09-01 14:21:53 -06:00
|
|
|
class Antenna(models.Model):
|
|
|
|
"""Model for antennas tracked with SatNOGS."""
|
2017-03-20 09:36:54 -06:00
|
|
|
frequency = models.PositiveIntegerField()
|
|
|
|
frequency_max = models.PositiveIntegerField()
|
2014-09-01 14:21:53 -06:00
|
|
|
band = models.CharField(choices=zip(ANTENNA_BANDS, ANTENNA_BANDS),
|
|
|
|
max_length=5)
|
|
|
|
antenna_type = models.CharField(choices=ANTENNA_TYPES, max_length=15)
|
|
|
|
|
2014-12-13 11:21:05 -07:00
|
|
|
def __unicode__(self):
|
2017-03-19 06:52:49 -06:00
|
|
|
return '{0} - {1} - {2} - {3}'.format(self.band, self.antenna_type,
|
|
|
|
self.frequency,
|
|
|
|
self.frequency_max)
|
2014-12-13 11:21:05 -07:00
|
|
|
|
2014-09-01 14:21:53 -06:00
|
|
|
|
|
|
|
class Station(models.Model):
|
|
|
|
"""Model for SatNOGS ground stations."""
|
2017-09-11 04:33:49 -06:00
|
|
|
owner = models.ForeignKey(User, related_name="ground_stations",
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
2014-09-01 14:21:53 -06:00
|
|
|
name = models.CharField(max_length=45)
|
2018-08-14 07:38:13 -06:00
|
|
|
image = models.ImageField(upload_to='ground_stations', blank=True,
|
|
|
|
validators=[validate_image])
|
2018-08-11 07:50:34 -06:00
|
|
|
alt = models.PositiveIntegerField(help_text='In meters above sea level')
|
2018-03-16 06:18:48 -06:00
|
|
|
lat = models.FloatField(validators=[MaxValueValidator(90), MinValueValidator(-90)],
|
|
|
|
help_text='eg. 38.01697')
|
|
|
|
lng = models.FloatField(validators=[MaxValueValidator(180), MinValueValidator(-180)],
|
|
|
|
help_text='eg. 23.7314')
|
2015-04-15 06:46:33 -06:00
|
|
|
qthlocator = models.CharField(max_length=255, blank=True)
|
|
|
|
location = models.CharField(max_length=255, blank=True)
|
2018-08-17 03:23:44 -06:00
|
|
|
antenna = models.ManyToManyField(Antenna, blank=True, related_name="stations",
|
2016-05-06 03:00:44 -06:00
|
|
|
help_text=('If you want to add a new Antenna contact '
|
|
|
|
'<a href="https://community.satnogs.org/" '
|
|
|
|
'target="_blank">SatNOGS Team</a>'))
|
2014-09-17 12:30:30 -06:00
|
|
|
featured_date = models.DateField(null=True, blank=True)
|
2014-12-01 13:20:38 -07:00
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
2018-03-07 08:46:35 -07:00
|
|
|
testing = models.BooleanField(default=True)
|
2015-05-06 02:53:48 -06:00
|
|
|
last_seen = models.DateTimeField(null=True, blank=True)
|
2017-11-17 07:31:54 -07:00
|
|
|
status = models.IntegerField(choices=STATION_STATUSES, default=0)
|
2016-03-18 19:59:59 -06:00
|
|
|
horizon = models.PositiveIntegerField(help_text='In degrees above 0', default=10)
|
2018-03-16 06:18:48 -06:00
|
|
|
description = models.TextField(max_length=500, blank=True, help_text='Max 500 characters')
|
2018-08-27 07:19:26 -06:00
|
|
|
client_version = models.CharField(max_length=45, blank=True)
|
2019-02-07 11:06:39 -07:00
|
|
|
target_utilization = models.IntegerField(validators=[MaxValueValidator(100),
|
|
|
|
MinValueValidator(0)],
|
|
|
|
help_text='Target utilization factor for '
|
2019-02-23 16:18:39 -07:00
|
|
|
'your station',
|
2019-02-07 11:06:39 -07:00
|
|
|
null=True, blank=True)
|
2014-09-17 12:30:30 -06:00
|
|
|
|
2015-05-06 04:06:19 -06:00
|
|
|
class Meta:
|
2017-11-17 07:31:54 -07:00
|
|
|
ordering = ['-status']
|
2015-05-06 04:06:19 -06:00
|
|
|
|
2015-02-05 17:35:27 -07:00
|
|
|
def get_image(self):
|
|
|
|
if self.image and hasattr(self.image, 'url'):
|
|
|
|
return self.image.url
|
|
|
|
else:
|
|
|
|
return settings.STATION_DEFAULT_IMAGE
|
|
|
|
|
2015-05-06 02:53:48 -06:00
|
|
|
@property
|
2017-11-17 07:31:54 -07:00
|
|
|
def is_online(self):
|
2015-05-06 02:53:48 -06:00
|
|
|
try:
|
2015-05-15 02:45:18 -06:00
|
|
|
heartbeat = self.last_seen + timedelta(minutes=int(settings.STATION_HEARTBEAT_TIME))
|
2017-11-17 07:31:54 -07:00
|
|
|
return heartbeat > now()
|
2017-11-17 07:31:21 -07:00
|
|
|
except TypeError:
|
2015-05-06 02:53:48 -06:00
|
|
|
return False
|
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
@property
|
|
|
|
def is_offline(self):
|
|
|
|
return not self.is_online
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_testing(self):
|
|
|
|
if self.is_online:
|
|
|
|
if self.status == 1:
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2015-05-06 04:06:19 -06:00
|
|
|
def state(self):
|
2017-11-17 07:31:54 -07:00
|
|
|
if not self.status:
|
|
|
|
return format_html('<span style="color:red;">Offline</span>')
|
|
|
|
if self.status == 1:
|
|
|
|
return format_html('<span style="color:orange;">Testing</span>')
|
|
|
|
return format_html('<span style="color:green">Online</span>')
|
2015-05-06 04:06:19 -06:00
|
|
|
|
2015-08-11 03:19:10 -06:00
|
|
|
@property
|
|
|
|
def success_rate(self):
|
2018-08-31 07:45:42 -06:00
|
|
|
rate = cache.get('station-{0}-rate'.format(self.id))
|
2018-03-07 11:59:51 -07:00
|
|
|
if not rate:
|
2018-08-31 07:45:42 -06:00
|
|
|
observations = self.observations.exclude(testing=True).exclude(vetted_status="unknown")
|
2018-08-16 14:51:34 -06:00
|
|
|
success = observations.filter(id__in=(o.id for o in observations
|
|
|
|
if o.is_good or o.is_bad)).count()
|
2018-03-07 11:59:51 -07:00
|
|
|
if observations:
|
|
|
|
rate = int(100 * (float(success) / float(observations.count())))
|
2018-08-31 07:45:42 -06:00
|
|
|
cache.set('station-{0}-rate'.format(self.id), rate)
|
2018-03-07 11:59:51 -07:00
|
|
|
else:
|
2018-03-10 07:26:08 -07:00
|
|
|
rate = False
|
|
|
|
return rate
|
2015-08-11 03:19:10 -06:00
|
|
|
|
2017-10-14 04:13:19 -06:00
|
|
|
@property
|
|
|
|
def observations_count(self):
|
|
|
|
count = self.observations.all().count()
|
|
|
|
return count
|
|
|
|
|
2017-12-12 07:53:35 -07:00
|
|
|
@property
|
|
|
|
def observations_future_count(self):
|
|
|
|
count = self.observations.is_future().count()
|
|
|
|
return count
|
|
|
|
|
2015-09-09 02:12:36 -06:00
|
|
|
@property
|
|
|
|
def apikey(self):
|
2019-05-02 14:05:25 -06:00
|
|
|
try:
|
|
|
|
token = Token.objects.get(user=self.owner)
|
|
|
|
except Token.DoesNotExist:
|
|
|
|
token = Token.objects.create(user=self.owner)
|
|
|
|
return token
|
2015-09-09 02:12:36 -06:00
|
|
|
|
2014-10-06 06:14:55 -06:00
|
|
|
def __unicode__(self):
|
|
|
|
return "%d - %s" % (self.pk, self.name)
|
|
|
|
|
2014-09-01 14:21:53 -06:00
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
post_save.connect(_station_post_save, sender=Station)
|
|
|
|
|
|
|
|
|
2018-03-23 07:28:23 -06:00
|
|
|
class StationStatusLog(models.Model):
|
|
|
|
station = models.ForeignKey(Station, related_name='station_logs',
|
|
|
|
on_delete=models.CASCADE, null=True, blank=True)
|
|
|
|
status = models.IntegerField(choices=STATION_STATUSES, default=0)
|
|
|
|
changed = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
class Meta:
|
2018-03-28 11:55:29 -06:00
|
|
|
ordering = ['-changed']
|
2018-03-23 07:28:23 -06:00
|
|
|
|
|
|
|
def __unicode__(self):
|
|
|
|
return '{0} - {1}'.format(self.station, self.status)
|
|
|
|
|
|
|
|
|
2014-09-01 14:21:53 -06:00
|
|
|
class Satellite(models.Model):
|
|
|
|
"""Model for SatNOGS satellites."""
|
|
|
|
norad_cat_id = models.PositiveIntegerField()
|
2018-12-04 12:16:43 -07:00
|
|
|
norad_follow_id = models.PositiveIntegerField(blank=True, null=True)
|
2014-09-01 14:21:53 -06:00
|
|
|
name = models.CharField(max_length=45)
|
2015-08-14 07:55:43 -06:00
|
|
|
names = models.TextField(blank=True)
|
2016-05-06 02:27:24 -06:00
|
|
|
image = models.CharField(max_length=100, blank=True, null=True)
|
2016-05-07 10:19:41 -06:00
|
|
|
manual_tle = models.BooleanField(default=False)
|
2017-08-12 10:03:51 -06:00
|
|
|
status = models.CharField(choices=zip(SATELLITE_STATUS, SATELLITE_STATUS),
|
|
|
|
max_length=10, default='alive')
|
2014-09-14 08:42:52 -06:00
|
|
|
|
2015-05-06 04:06:19 -06:00
|
|
|
class Meta:
|
|
|
|
ordering = ['norad_cat_id']
|
|
|
|
|
2015-08-14 07:55:43 -06:00
|
|
|
def get_image(self):
|
2016-04-08 04:14:36 -06:00
|
|
|
if self.image:
|
|
|
|
return self.image
|
2015-08-14 07:55:43 -06:00
|
|
|
else:
|
|
|
|
return settings.SATELLITE_DEFAULT_IMAGE
|
|
|
|
|
2014-09-17 10:53:25 -06:00
|
|
|
def __unicode__(self):
|
|
|
|
return self.name
|
|
|
|
|
2014-09-14 08:42:52 -06:00
|
|
|
|
2016-01-22 10:48:07 -07:00
|
|
|
class Tle(models.Model):
|
|
|
|
tle0 = models.CharField(max_length=100, blank=True)
|
|
|
|
tle1 = models.CharField(max_length=200, blank=True)
|
|
|
|
tle2 = models.CharField(max_length=200, blank=True)
|
|
|
|
updated = models.DateTimeField(auto_now=True, blank=True)
|
2017-09-11 04:33:49 -06:00
|
|
|
satellite = models.ForeignKey(Satellite, related_name='tles',
|
|
|
|
on_delete=models.CASCADE, null=True, blank=True)
|
2016-01-22 10:48:07 -07:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
ordering = ['tle0']
|
|
|
|
|
|
|
|
def __unicode__(self):
|
2018-12-05 10:29:44 -07:00
|
|
|
uni_name = "%d - %s" % (self.id, self.tle0)
|
|
|
|
return uni_name
|
2016-01-22 10:48:07 -07:00
|
|
|
|
2018-12-11 07:02:05 -07:00
|
|
|
@property
|
|
|
|
def str_array(self):
|
|
|
|
# tle fields are unicode, pyephem and others expect python strings
|
|
|
|
return [str(self.tle0), str(self.tle1), str(self.tle2)]
|
|
|
|
|
2016-01-22 10:48:07 -07:00
|
|
|
|
2018-12-05 14:57:39 -07:00
|
|
|
post_save.connect(_tle_post_save, sender=Tle)
|
|
|
|
|
|
|
|
|
2019-07-24 03:34:15 -06:00
|
|
|
class LatestTleManager(models.Manager):
|
|
|
|
"""Django Manager for latest Tle objects"""
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
"""Returns query of latest Tle
|
|
|
|
|
|
|
|
:returns: the latest Tle for each Satellite
|
|
|
|
"""
|
|
|
|
subquery = Tle.objects.filter(satellite=OuterRef('satellite')).order_by('-updated')
|
|
|
|
return super(LatestTleManager, self).get_queryset().filter(
|
|
|
|
updated=Subquery(subquery.values('updated')[:1]))
|
|
|
|
|
|
|
|
|
|
|
|
class LatestTle(Tle):
|
|
|
|
"""LatestTle is the latest entry of a Satellite Tle objects
|
|
|
|
"""
|
|
|
|
objects = LatestTleManager()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
proxy = True
|
|
|
|
|
|
|
|
|
2015-07-23 09:18:01 -06:00
|
|
|
class Transmitter(models.Model):
|
2014-09-14 08:42:52 -06:00
|
|
|
"""Model for antennas transponders."""
|
2015-05-09 03:50:07 -06:00
|
|
|
uuid = ShortUUIDField(db_index=True)
|
2018-09-02 14:05:15 -06:00
|
|
|
sync_to_db = models.BooleanField(default=False)
|
2014-09-01 14:21:53 -06:00
|
|
|
|
|
|
|
|
|
|
|
class Observation(models.Model):
|
|
|
|
"""Model for SatNOGS observations."""
|
2017-09-11 04:33:49 -06:00
|
|
|
satellite = models.ForeignKey(Satellite, related_name='observations',
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
tle = models.ForeignKey(Tle, related_name='observations',
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
author = models.ForeignKey(User, related_name='observations',
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
2014-09-01 14:21:53 -06:00
|
|
|
start = models.DateTimeField()
|
|
|
|
end = models.DateTimeField()
|
2017-09-11 04:33:49 -06:00
|
|
|
ground_station = models.ForeignKey(Station, related_name='observations',
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
2018-02-02 09:29:40 -07:00
|
|
|
client_version = models.CharField(max_length=255, blank=True)
|
|
|
|
client_metadata = models.TextField(blank=True)
|
2018-01-14 03:49:18 -07:00
|
|
|
payload = models.FileField(upload_to=_name_obs_files, blank=True, null=True)
|
|
|
|
waterfall = models.ImageField(upload_to=_name_obs_files, blank=True, null=True)
|
2017-09-11 04:33:49 -06:00
|
|
|
vetted_datetime = models.DateTimeField(null=True, blank=True)
|
|
|
|
vetted_user = models.ForeignKey(User, related_name='observations_vetted',
|
|
|
|
on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
vetted_status = models.CharField(choices=OBSERVATION_STATUSES,
|
|
|
|
max_length=20, default='unknown')
|
2017-11-17 07:31:54 -07:00
|
|
|
testing = models.BooleanField(default=False)
|
2017-09-11 04:33:49 -06:00
|
|
|
rise_azimuth = models.FloatField(blank=True, null=True)
|
|
|
|
max_altitude = models.FloatField(blank=True, null=True)
|
|
|
|
set_azimuth = models.FloatField(blank=True, null=True)
|
2017-11-24 15:38:25 -07:00
|
|
|
archived = models.BooleanField(default=False)
|
|
|
|
archive_identifier = models.CharField(max_length=255, blank=True)
|
|
|
|
archive_url = models.URLField(blank=True, null=True)
|
2019-06-21 12:14:52 -06:00
|
|
|
transmitter_uuid = ShortUUIDField(auto=False, db_index=True)
|
2019-05-02 14:05:25 -06:00
|
|
|
transmitter_description = models.TextField(default='')
|
|
|
|
transmitter_type = models.CharField(choices=zip(TRANSMITTER_TYPE, TRANSMITTER_TYPE),
|
|
|
|
max_length=11, default='Transmitter')
|
|
|
|
transmitter_uplink_low = models.BigIntegerField(blank=True, null=True)
|
|
|
|
transmitter_uplink_high = models.BigIntegerField(blank=True, null=True)
|
|
|
|
transmitter_uplink_drift = models.IntegerField(blank=True, null=True)
|
|
|
|
transmitter_downlink_low = models.BigIntegerField(blank=True, null=True)
|
|
|
|
transmitter_downlink_high = models.BigIntegerField(blank=True, null=True)
|
|
|
|
transmitter_downlink_drift = models.IntegerField(blank=True, null=True)
|
2019-05-03 19:02:19 -06:00
|
|
|
transmitter_mode = models.CharField(max_length=10, blank=True, null=True)
|
2019-05-02 14:05:25 -06:00
|
|
|
transmitter_invert = models.BooleanField(default=False)
|
|
|
|
transmitter_baud = models.FloatField(validators=[MinValueValidator(0)], blank=True, null=True)
|
|
|
|
transmitter_created = models.DateTimeField(default=now)
|
2015-05-06 04:06:19 -06:00
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
objects = ObservationManager.as_manager()
|
|
|
|
|
2014-09-08 11:36:12 -06:00
|
|
|
@property
|
|
|
|
def is_past(self):
|
|
|
|
return self.end < now()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_future(self):
|
|
|
|
return self.end > now()
|
|
|
|
|
2019-05-31 11:49:07 -06:00
|
|
|
@property
|
|
|
|
def is_started(self):
|
|
|
|
return self.start < now()
|
|
|
|
|
2017-12-24 12:17:37 -07:00
|
|
|
# this payload has been vetted good/bad/failed by someone
|
Initial data vetting/verification system
Model change (with migration 0006) adds 3 fields to Data:
vetted_status (charfield with options for data status, default "unknown")
vetted_user (who vetted the data)
vetted_datetime (when it was vetted)
In addition, various boolean functions are added for the Data model
to check statuses. More functions are added to the Observation model
to check status of verification within an observation as well, assuming
multiple data entries in an Observation. With these, I also changed
"has_data" to "has_submitted_data" to be more specific alongside the
others.
For UX, we add a green check sign or red removal sign to the data
header in Observation view (along with green/red datetime in the footer)
if a data is verified good or bad, respectively. If there is an unknown
status, the data header is given a thumbs-up and thumbs-down button to
verify the data good or bad. These icons are only offered to is_staff,
the observation requestor, and any station owner in the observation.
These buttons trigger new URLs/functions in view:
data_verify(id)
data_mark_bad(id)
Returning the user back to the originating Observation page.
In the observation lists I changed the coloring of the ID button to be:
Future: light blue (same)
No uploaded data and/or all vetted bad data: red
Some or all unvetted data with no verified good data: orange
Some or all verified good data: green
These changes are reflected in the observations.html, home.html, and
user_detail.html templates.
solves satnogs/satnogs-network#171
2016-03-25 13:52:45 -06:00
|
|
|
@property
|
|
|
|
def is_vetted(self):
|
|
|
|
return not self.vetted_status == 'unknown'
|
|
|
|
|
|
|
|
# this payload has been vetted as good by someone
|
|
|
|
@property
|
2017-12-24 12:17:37 -07:00
|
|
|
def is_good(self):
|
|
|
|
return self.vetted_status == 'good'
|
Initial data vetting/verification system
Model change (with migration 0006) adds 3 fields to Data:
vetted_status (charfield with options for data status, default "unknown")
vetted_user (who vetted the data)
vetted_datetime (when it was vetted)
In addition, various boolean functions are added for the Data model
to check statuses. More functions are added to the Observation model
to check status of verification within an observation as well, assuming
multiple data entries in an Observation. With these, I also changed
"has_data" to "has_submitted_data" to be more specific alongside the
others.
For UX, we add a green check sign or red removal sign to the data
header in Observation view (along with green/red datetime in the footer)
if a data is verified good or bad, respectively. If there is an unknown
status, the data header is given a thumbs-up and thumbs-down button to
verify the data good or bad. These icons are only offered to is_staff,
the observation requestor, and any station owner in the observation.
These buttons trigger new URLs/functions in view:
data_verify(id)
data_mark_bad(id)
Returning the user back to the originating Observation page.
In the observation lists I changed the coloring of the ID button to be:
Future: light blue (same)
No uploaded data and/or all vetted bad data: red
Some or all unvetted data with no verified good data: orange
Some or all verified good data: green
These changes are reflected in the observations.html, home.html, and
user_detail.html templates.
solves satnogs/satnogs-network#171
2016-03-25 13:52:45 -06:00
|
|
|
|
|
|
|
# this payload has been vetted as bad by someone
|
|
|
|
@property
|
2017-12-24 12:17:37 -07:00
|
|
|
def is_bad(self):
|
|
|
|
return self.vetted_status == 'bad'
|
|
|
|
|
|
|
|
# this payload has been vetted as failed by someone
|
|
|
|
@property
|
|
|
|
def is_failed(self):
|
|
|
|
return self.vetted_status == 'failed'
|
Initial data vetting/verification system
Model change (with migration 0006) adds 3 fields to Data:
vetted_status (charfield with options for data status, default "unknown")
vetted_user (who vetted the data)
vetted_datetime (when it was vetted)
In addition, various boolean functions are added for the Data model
to check statuses. More functions are added to the Observation model
to check status of verification within an observation as well, assuming
multiple data entries in an Observation. With these, I also changed
"has_data" to "has_submitted_data" to be more specific alongside the
others.
For UX, we add a green check sign or red removal sign to the data
header in Observation view (along with green/red datetime in the footer)
if a data is verified good or bad, respectively. If there is an unknown
status, the data header is given a thumbs-up and thumbs-down button to
verify the data good or bad. These icons are only offered to is_staff,
the observation requestor, and any station owner in the observation.
These buttons trigger new URLs/functions in view:
data_verify(id)
data_mark_bad(id)
Returning the user back to the originating Observation page.
In the observation lists I changed the coloring of the ID button to be:
Future: light blue (same)
No uploaded data and/or all vetted bad data: red
Some or all unvetted data with no verified good data: orange
Some or all verified good data: green
These changes are reflected in the observations.html, home.html, and
user_detail.html templates.
solves satnogs/satnogs-network#171
2016-03-25 13:52:45 -06:00
|
|
|
|
2018-09-11 09:32:05 -06:00
|
|
|
@property
|
|
|
|
def has_waterfall(self):
|
|
|
|
"""Run some checks on the waterfall for existence of data."""
|
|
|
|
if self.waterfall is None:
|
|
|
|
return False
|
|
|
|
if not os.path.isfile(os.path.join(settings.MEDIA_ROOT, self.waterfall.name)):
|
|
|
|
return False
|
|
|
|
if self.waterfall.size == 0:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2017-02-09 05:33:44 -07:00
|
|
|
@property
|
2017-12-18 16:11:36 -07:00
|
|
|
def has_audio(self):
|
2017-09-27 11:04:18 -06:00
|
|
|
"""Run some checks on the payload for existence of data."""
|
2017-12-19 03:41:52 -07:00
|
|
|
if self.archive_url:
|
|
|
|
return True
|
2017-02-09 05:33:44 -07:00
|
|
|
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
|
|
|
|
|
2017-12-27 06:15:02 -07:00
|
|
|
@property
|
|
|
|
def has_demoddata(self):
|
|
|
|
"""Check if the observation has Demod Data."""
|
|
|
|
if self.demoddata.count():
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2017-12-18 16:11:36 -07:00
|
|
|
@property
|
|
|
|
def audio_url(self):
|
|
|
|
if self.has_audio:
|
|
|
|
if self.archive_url:
|
2018-09-04 09:19:54 -06:00
|
|
|
try:
|
2019-06-25 06:03:06 -06:00
|
|
|
r = requests.get(self.archive_url, allow_redirects=False)
|
2018-09-04 09:19:54 -06:00
|
|
|
url = r.headers['Location']
|
|
|
|
return url
|
2019-06-25 06:03:06 -06:00
|
|
|
except Exception as e:
|
2018-09-04 09:19:54 -06:00
|
|
|
logger = logging.getLogger(__name__)
|
2019-06-25 06:03:06 -06:00
|
|
|
logger.warning("Error in request to '%s'. Error: %s",
|
|
|
|
self.archive_url, e)
|
2018-09-04 09:19:54 -06:00
|
|
|
return ''
|
2017-12-18 16:11:36 -07:00
|
|
|
else:
|
|
|
|
return self.payload.url
|
|
|
|
return ''
|
|
|
|
|
2015-05-06 04:06:19 -06:00
|
|
|
class Meta:
|
|
|
|
ordering = ['-start', '-end']
|
2016-04-25 08:54:06 -06:00
|
|
|
|
2017-09-11 04:33:49 -06:00
|
|
|
def __unicode__(self):
|
|
|
|
return str(self.id)
|
|
|
|
|
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse('base:observation_view', kwargs={'id': self.id})
|
|
|
|
|
2016-04-25 08:54:06 -06:00
|
|
|
|
2017-09-11 04:33:49 -06:00
|
|
|
@receiver(models.signals.post_delete, sender=Observation)
|
|
|
|
def observation_remove_files(sender, instance, **kwargs):
|
2017-08-22 13:07:23 -06:00
|
|
|
if instance.payload:
|
|
|
|
if os.path.isfile(instance.payload.path):
|
|
|
|
os.remove(instance.payload.path)
|
|
|
|
if instance.waterfall:
|
|
|
|
if os.path.isfile(instance.waterfall.path):
|
|
|
|
os.remove(instance.waterfall.path)
|
|
|
|
|
|
|
|
|
2017-11-17 07:31:54 -07:00
|
|
|
post_save.connect(_observation_post_save, sender=Observation)
|
2017-12-27 06:15:02 -07:00
|
|
|
|
|
|
|
|
2016-04-25 08:54:06 -06:00
|
|
|
class DemodData(models.Model):
|
2017-09-11 04:33:49 -06:00
|
|
|
observation = models.ForeignKey(Observation, related_name='demoddata',
|
2019-03-07 12:41:22 -07:00
|
|
|
on_delete=models.CASCADE)
|
|
|
|
payload_demod = models.FileField(upload_to=_name_obs_demoddata, unique=True)
|
2018-09-02 14:05:15 -06:00
|
|
|
copied_to_db = models.BooleanField(default=False)
|
2017-02-10 11:10:19 -07:00
|
|
|
|
2017-08-27 05:36:56 -06:00
|
|
|
def is_image(self):
|
|
|
|
with open(self.payload_demod.path) as fp:
|
|
|
|
try:
|
|
|
|
Image.open(fp)
|
2017-11-17 07:31:21 -07:00
|
|
|
except (IOError, TypeError):
|
2017-08-27 05:36:56 -06:00
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2017-02-10 11:10:19 -07:00
|
|
|
def display_payload(self):
|
|
|
|
with open(self.payload_demod.path) as fp:
|
2018-04-01 08:11:16 -06:00
|
|
|
payload = fp.read()
|
2018-03-26 07:38:31 -06:00
|
|
|
try:
|
2018-04-01 08:11:16 -06:00
|
|
|
return unicode(payload)
|
2018-03-26 07:38:31 -06:00
|
|
|
except UnicodeDecodeError:
|
2018-04-01 08:11:16 -06:00
|
|
|
data = payload.encode('hex').upper()
|
2018-03-26 07:38:31 -06:00
|
|
|
return ' '.join(data[i:i + 2] for i in xrange(0, len(data), 2))
|
2017-08-22 13:07:23 -06:00
|
|
|
|
|
|
|
|
|
|
|
@receiver(models.signals.post_delete, sender=DemodData)
|
|
|
|
def demoddata_remove_files(sender, instance, **kwargs):
|
|
|
|
if instance.payload_demod:
|
|
|
|
if os.path.isfile(instance.payload_demod.path):
|
|
|
|
os.remove(instance.payload_demod.path)
|