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

298 lines
11 KiB
Python

"""SatNOGS DB API django rest framework Views"""
from django.core.files.base import ContentFile
from django.db.models import F
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import 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 Artifact, DemodData, LatestTleSet, Mode, Satellite, Transmitter
from db.base.tasks import update_satellite
@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='norad_cat_id',
description='Select a satellite by its NORAD-assigned identifier'
),
OpenApiParameter(
name='status',
description='Filter satellites by their operational status',
required=False,
type=bool
),
],
),
retrieve=extend_schema(
description='Retrieve details on a single satellite in SatNOGS DB',
parameters=[
OpenApiParameter(
'norad_cat_id',
OpenApiTypes.INT64,
OpenApiParameter.PATH,
description='Select a satellite by its NORAD-assigned 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.all()
serializer_class = serializers.SatelliteSerializer
filterset_class = filters.SatelliteViewFilter
lookup_field = 'norad_cat_id'
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.all()
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'
),
OpenApiParameter(name='transmitter', description='Not currently in use'),
],
),
)
class TelemetryViewSet( # pylint: disable=R0901
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()
serializer_class = serializers.TelemetrySerializer
filterset_class = filters.TelemetryViewFilter
permission_classes = [SafeMethodsWithPermission]
parser_classes = (FormParser, FileUploadParser)
pagination_class = pagination.LinkedHeaderPageNumberPagination
@extend_schema(
responses={'201': None}, # None
)
def create(self, request, *args, **kwargs):
"""
Creates an frame of telemetry data from a satellite observation.
"""
data = {}
norad_cat_id = request.data.get('noradID')
if not Satellite.objects.filter(norad_cat_id=norad_cat_id).exists():
try:
update_satellite(norad_cat_id, update_name=True)
except LookupError:
return Response(status=status.HTTP_400_BAD_REQUEST)
data['satellite'] = Satellite.objects.get(norad_cat_id=norad_cat_id).id
data['station'] = request.data.get('source')
timestamp = request.data.get('timestamp')
data['timestamp'] = timestamp
# Convert coordinates to omit N-S and W-E designators
lat = request.data.get('latitude')
lng = request.data.get('longitude')
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)
# 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)
headers = self.get_success_headers(serializer.data)
return Response(status=status.HTTP_201_CREATED, headers=headers)
@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