1
0
Fork 0
satnogs-db/db/api/views.py

401 lines
16 KiB
Python

"""SatNOGS DB API django rest framework Views"""
from django.conf import settings
from django.core.files.base import ContentFile
from django.db.models import F
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema, \
extend_schema_view
from rest_framework import mixins, status, viewsets
from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from db.api import filters, pagination, serializers
from db.api.perms import SafeMethodsWithPermission
from db.api.renderers import BrowserableJSONLDRenderer, JSONLDRenderer
from db.base.helpers import gridsquare
from db.base.models import SATELLITE_STATUS, SERVICE_TYPE, TRANSMITTER_STATUS, TRANSMITTER_TYPE, \
Artifact, DemodData, LatestTleSet, Mode, Satellite, SatelliteEntry, SatelliteIdentifier, \
Transmitter
from db.base.tasks import decode_current_frame, publish_current_frame, update_satellite_name
ISS_EXAMPLE = OpenApiExample('25544 (ISS)', value=25544)
@extend_schema_view(
retrieve=extend_schema(
description='Retrieve a single RF Mode from SatNOGS DB based on its ID',
),
list=extend_schema(description='Retrieve a complete list of RF Modes from SatNOGS DB', )
)
class ModeViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
"""
Read-only view into the transmitter modulation modes (RF Modes) currently tracked
in the SatNOGS DB database
For more details on individual RF mode types please [see our wiki][moderef].
[moderef]: https://wiki.satnogs.org/Category:RF_Modes
"""
renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
]
queryset = Mode.objects.all()
serializer_class = serializers.ModeSerializer
@extend_schema_view(
list=extend_schema(
description='Retrieve a full or filtered list of satellites in SatNOGS DB',
parameters=[
# drf-spectacular does not currently recognize the in_orbit filter as a
# bool, forcing it here. See drf-spectacular#234
OpenApiParameter(
name='in_orbit',
description='Filter by satellites currently in orbit (True) or those that have \
decayed (False)',
required=False,
type=bool
),
OpenApiParameter(
name='status',
description='Filter by satellite status: ' + ' '.join(SATELLITE_STATUS),
required=False,
type=OpenApiTypes.STR
),
OpenApiParameter(
name='norad_cat_id',
description='Select a satellite by its NORAD-assigned identifier',
examples=[ISS_EXAMPLE],
),
],
),
retrieve=extend_schema(
description='Retrieve details on a single satellite in SatNOGS DB',
parameters=[
OpenApiParameter(
'satellite_identifier__sat_id',
OpenApiTypes.STR,
OpenApiParameter.PATH,
description='Select a satellite by its Satellite Identifier',
),
],
),
)
class SatelliteViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
"""
Read-only view into the Satellite entities in the SatNOGS DB database
"""
renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
]
queryset = Satellite.objects.filter(
associated_satellite__isnull=True, satellite_entry__approved=True
).prefetch_related('associated_with', 'telemetries')
serializer_class = serializers.SatelliteSerializer
filterset_class = filters.SatelliteViewFilter
lookup_field = 'satellite_identifier__sat_id'
def get_object(self):
queryset = self.get_queryset()
# Apply any filter backends
queryset = self.filter_queryset(queryset)
# In case user uses NORAD ID for getting satellite
if 'satellite_entry__norad_cat_id' in self.kwargs:
norad_cat_id = self.kwargs['satellite_entry__norad_cat_id']
return get_object_or_404(queryset, satellite_entry__norad_cat_id=norad_cat_id)
# Getting satellite by using Satellite Identifier
sat_id = self.kwargs['satellite_identifier__sat_id']
try:
return queryset.get(satellite_identifier__sat_id=sat_id)
except Satellite.DoesNotExist:
return get_object_or_404(
queryset, associated_with__satellite_identifier__sat_id=sat_id
)
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(
name='satellite__norad_cat_id',
description='NORAD ID of a satellite to filter telemetry data for',
examples=[ISS_EXAMPLE],
),
OpenApiParameter(
name='status',
description='Filter by transmitter status: ' + ' '.join(TRANSMITTER_STATUS),
required=False,
type=OpenApiTypes.STR,
examples=[OpenApiExample('active', value='\'active\'')]
),
OpenApiParameter(
name='service',
description='Filter by transmitter service: ' + ' '.join(SERVICE_TYPE),
required=False,
type=OpenApiTypes.STR,
examples=[OpenApiExample('Amateur', value='\'Amateur\'')]
),
OpenApiParameter(
name='type',
description='Filter by transmitter type: ' + ' '.join(TRANSMITTER_TYPE),
required=False,
type=OpenApiTypes.STR,
examples=[OpenApiExample('Transmitter', value='\'Transmitter\'')]
),
],
),
)
class TransmitterViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
"""
Read-only view into the Transmitter entities in the SatNOGS DB database.
Transmitters are inclusive of Transceivers and Transponders
"""
renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
]
queryset = Transmitter.objects.filter(
satellite__associated_satellite__isnull=True, satellite__satellite_entry__approved=True
)
serializer_class = serializers.TransmitterSerializer
filterset_class = filters.TransmitterViewFilter
lookup_field = 'uuid'
class LatestTleSetViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
"""
Read-only view into the most recent two-line elements (TLE) in the SatNOGS DB
database
"""
renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
queryset = LatestTleSet.objects.all().select_related('satellite').exclude(
latest_distributable__isnull=True
).annotate(
tle0=F('latest_distributable__tle0'),
tle1=F('latest_distributable__tle1'),
tle2=F('latest_distributable__tle2'),
tle_source=F('latest_distributable__tle_source'),
updated=F('latest_distributable__updated')
)
serializer_class = serializers.LatestTleSetSerializer
filterset_class = filters.LatestTleSetViewFilter
def get_queryset(self):
"""
Returns latest TLE queryset depending on user permissions
"""
if self.request.user.has_perm('base.access_all_tles'):
return LatestTleSet.objects.all().select_related('satellite').exclude(
latest__isnull=True
).annotate(
tle0=F('latest__tle0'),
tle1=F('latest__tle1'),
tle2=F('latest__tle2'),
tle_source=F('latest__tle_source'),
updated=F('latest__updated')
)
return self.queryset
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(
name='app_source',
description='The submission source for the telemetry frames: manual (a manual \
upload/entry), network (SatNOGS Network observations), or sids \
(legacy API submission)',
),
OpenApiParameter(
name='observer',
description='(string) name of the observer (submitter) to retrieve telemetry data \
from'
),
OpenApiParameter(
name='satellite',
description='NORAD ID of a satellite to filter telemetry data for',
examples=[ISS_EXAMPLE],
),
OpenApiParameter(name='transmitter', description='Not currently in use'),
],
),
)
class TelemetryViewSet( # pylint: disable=R0901,R0912,R0915
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
View into the Telemetry objects in the SatNOGS DB database. Currently,
this table is inclusive of all data collected from satellite downlink
observations
"""
renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
]
queryset = DemodData.objects.all().select_related('satellite', 'transmitter')
serializer_class = serializers.TelemetrySerializer
filterset_class = filters.TelemetryViewFilter
permission_classes = [SafeMethodsWithPermission]
parser_classes = (FormParser, MultiPartParser, FileUploadParser)
pagination_class = pagination.LinkedHeaderPageNumberPagination
@extend_schema(
responses={'201': None}, # None
)
def create(self, request, *args, **kwargs):
"""
Creates a frame of telemetry data from a satellite observation.
"""
# pylint: disable=R0914
data = {}
norad_id = request.data.get('noradID')
try:
satellite = Satellite.objects.get(satellite_entry__norad_cat_id=norad_id)
except Satellite.DoesNotExist:
try:
satellite_identifier = SatelliteIdentifier.objects.create()
satellite_entry = SatelliteEntry.objects.create(
norad_cat_id=norad_id,
name='New Satellite',
satellite_identifier=satellite_identifier,
created=now()
)
satellite = Satellite.objects.create(
satellite_identifier=satellite_identifier, satellite_entry=satellite_entry
)
update_satellite_name.delay(int(norad_id))
except ValueError:
return Response(status=status.HTTP_400_BAD_REQUEST)
data['satellite'] = satellite.id
data['station'] = request.data.get('source')
data['timestamp'] = request.data.get('timestamp')
if request.data.get('version'):
data['version'] = request.data.get('version')
observation_id = ''
if request.data.get('observation_id'):
observation_id = request.data.get('observation_id')
data['observation_id'] = observation_id
station_id = ''
if request.data.get('station_id'):
station_id = request.data.get('station_id')
data['station_id'] = station_id
# Convert coordinates to omit N-S and W-E designators
lat = request.data.get('latitude')
lng = request.data.get('longitude')
try:
if any(x.isalpha() for x in lat):
data['lat'] = (-float(lat[:-1]) if ('S' in lat) else float(lat[:-1]))
else:
data['lat'] = float(lat)
if any(x.isalpha() for x in lng):
data['lng'] = (-float(lng[:-1]) if ('W' in lng) else float(lng[:-1]))
else:
data['lng'] = float(lng)
except ValueError:
return Response(status=status.HTTP_400_BAD_REQUEST)
# Network or SiDS submission?
if request.data.get('satnogs_network'):
data['app_source'] = 'network'
else:
data['app_source'] = 'sids'
# Create file out of frame string
frame = ContentFile(request.data.get('frame'), name='sids')
data['payload_frame'] = frame
# Create observer
qth = gridsquare(data['lat'], data['lng'])
observer = '{0}-{1}'.format(data['station'], qth)
data['observer'] = observer
serializer = serializers.SidsSerializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
# Run task to decode the current frame
decode_current_frame.delay(satellite.satellite_identifier.sat_id, serializer.instance.pk)
# Run task to publish the current frame via ZeroMQ
if settings.ZEROMQ_ENABLE:
publish_current_frame.delay(
request.data.get('timestamp'), request.data.get('frame'), observer, {
'norad_id': norad_id,
'observation_id': observation_id,
'station_id': station_id
}
)
return Response(status=status.HTTP_201_CREATED)
@extend_schema_view(
list=extend_schema(
parameters=[
OpenApiParameter(
'network_obs_id',
OpenApiTypes.INT64,
required=False,
description='Given a SatNOGS Network observation ID, this will return any \
artifacts files associated with the observation.'
),
],
),
retrieve=extend_schema(
parameters=[
OpenApiParameter(
'id',
OpenApiTypes.URI,
OpenApiParameter.PATH,
description='The ID for the requested artifact entry in DB'
),
],
),
)
class ArtifactViewSet( # pylint: disable=R0901
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
Artifacts are file-formatted objects collected from a satellite observation.
"""
queryset = Artifact.objects.all()
filterset_class = filters.ArtifactViewFilter
permission_classes = [IsAuthenticated]
parser_classes = (FormParser, MultiPartParser)
pagination_class = pagination.LinkedHeaderPageNumberPagination
def get_serializer_class(self):
"""Returns the right serializer depending on http method that is used"""
if self.action == 'create':
return serializers.NewArtifactSerializer
return serializers.ArtifactSerializer
def create(self, request, *args, **kwargs):
"""
Creates observation artifact from an [HDF5 formatted file][hdf5ref]
* Requires session or key authentication to create an artifact
[hdf5ref]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format
"""
serializer = self.get_serializer(data=request.data)
try:
if serializer.is_valid():
data = serializer.save()
http_response = {}
http_response['id'] = data.id
response = Response(http_response, status=status.HTTP_200_OK)
else:
data = serializer.errors
response = Response(data, status=status.HTTP_400_BAD_REQUEST)
except (ValidationError, ValueError, OSError) as error:
response = Response(str(error), status=status.HTTP_400_BAD_REQUEST)
return response