diff --git a/db/api/parsers.py b/db/api/parsers.py new file mode 100644 index 0000000..8a79196 --- /dev/null +++ b/db/api/parsers.py @@ -0,0 +1,18 @@ +"""SatNOGS DB django rest framework API custom parsers""" +from pyld import jsonld +from rest_framework.parsers import JSONParser + +from db.base.structured_data import get_structured_data + + +class JSONLDParser(JSONParser): # pylint: disable=R0903 + """ Parser for JSONLD. """ + + media_type = 'application/ld+json' + + def parse(self, stream, media_type=None, parser_context=None): + """ Render `data` into JSONLD, returning a bytestring. """ + raw_data = super().parse(stream, media_type, parser_context) + structured_data = get_structured_data(parser_context['view'].basename, []) + data = jsonld.frame(raw_data, structured_data.frame, {'omitGraph': False}) + return data diff --git a/db/api/serializers.py b/db/api/serializers.py index 7f669c6..67b06e4 100644 --- a/db/api/serializers.py +++ b/db/api/serializers.py @@ -8,7 +8,7 @@ from drf_spectacular.utils import OpenApiExample, extend_schema_field, extend_sc from rest_framework import serializers from db.base.models import TRANSMITTER_STATUS, Artifact, DemodData, LatestTleSet, Mode, \ - Satellite, Telemetry, Transmitter + Satellite, Telemetry, Transmitter, TransmitterEntry @extend_schema_serializer( @@ -181,6 +181,18 @@ class SatelliteSerializer(serializers.ModelSerializer): ] +class TransmitterEntrySerializer(serializers.ModelSerializer): + """SatNOGS DB TransmitterEntry API Serializer""" + class Meta: + model = TransmitterEntry + fields = ( + 'uuid', 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'uplink_drift', + 'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode', 'uplink_mode', + 'invert', 'baud', 'satellite', 'citation', 'service', 'coordination', + 'coordination_url', 'created_by' + ) + + @extend_schema_serializer( examples=[ OpenApiExample( diff --git a/db/api/views.py b/db/api/views.py index 3e6f6a7..33aa79f 100644 --- a/db/api/views.py +++ b/db/api/views.py @@ -15,6 +15,7 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError from db.api import filters, pagination, serializers +from db.api.parsers import JSONLDParser from db.api.perms import SafeMethodsWithPermission from db.api.renderers import BrowserableJSONLDRenderer, JSONLDRenderer from db.base.helpers import gridsquare @@ -152,14 +153,17 @@ class SatelliteViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 ], ), ) -class TransmitterViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 +class TransmitterViewSet( # pylint: disable=R0901 + mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, + viewsets.GenericViewSet): """ - Read-only view into the Transmitter entities in the SatNOGS DB database. + View into the Transmitter entities in the SatNOGS DB database. Transmitters are inclusive of Transceivers and Transponders """ renderer_classes = [ JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer ] + parser_classes = [JSONLDParser] queryset = Transmitter.objects.filter( satellite__associated_satellite__isnull=True, satellite__satellite_entry__approved=True ) @@ -167,6 +171,170 @@ class TransmitterViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R090 filterset_class = filters.TransmitterViewFilter lookup_field = 'uuid' + def create(self, request, *args, **kwargs): # noqa: C901; pylint: disable=R0911,R0912,R0915 + """ + Creates a transmitter suggestion. + """ + transmitters_data = [] + for transmitter_entry in request.data['@graph']: + if 'transmitter' not in transmitter_entry: + data = 'Transmitter Entry without "transmitter" key' + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if not transmitter_entry['transmitter']: + data = 'One or more of the required fields are missing.\n Required fields: \ + description, status, citation, service, satellite' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + transmitter = transmitter_entry['transmitter'] + + transmitter_data = {} + if "@id" not in transmitter: + data = 'Missing "@id" for one or more entries' + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + if 'uuid' in transmitter: + if isinstance(transmitter['uuid'], list): + data = 'Multiple values for "http://schema.org/identifier" or multiple \ + entries with the same "@id" and different \ + "http://schema.org/identifier" values' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + transmitter_uuid = transmitter['uuid'] + if Transmitter.objects.filter(uuid=transmitter_uuid).exists(): + transmitter_data['uuid'] = transmitter_uuid + + transmitter_data['description'] = transmitter['description'] + transmitter_data['status'] = transmitter['status'] + transmitter_data['citation'] = transmitter['citation'] + transmitter_data['service'] = transmitter['service'] + transmitter_data['coordination'] = '' + transmitter_data['coordination_url'] = '' + transmitter_data['created_by'] = request.user.pk + + try: + if transmitter['satellite']: + if isinstance(transmitter['satellite'], list): + data = 'Multiple values for "https://schema.space/metasat/satellite" \ + or multiple entries with the same "@id" and different \ + "https://schema.space/metasat/satellite" values' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + transmitter_data['satellite'] = Satellite.objects.get( + satellite_entry__norad_cat_id=transmitter['satellite']['norad_cat_id'] + ).pk + else: + data = 'Missing NORAD ID value for Satellite' + return Response(data, status=status.HTTP_400_BAD_REQUEST) + except Satellite.DoesNotExist: + data = 'Unknown NORAD ID: {}'.format(transmitter['satellite']['norad_cat_id']) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + if 'baud' in transmitter: + transmitter_data['baud'] = transmitter['baud'] + + if 'invert' in transmitter: + transmitter_data['invert'] = transmitter['invert'] + + if 'uplink' not in transmitter and 'downlink' not in transmitter: + data = 'Missing "https://schema.space/metasat/uplink" or \ + "https://schema.space/metasat/downlink"' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + if 'uplink' in transmitter: + if isinstance(transmitter['uplink'], list): + data = 'Multiple values for "https://schema.space/metasat/uplink" or multiple \ + entries with the same "@id" and different \ + "https://schema.space/metasat/uplink" values' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if 'frequency' not in transmitter['uplink']: + data = 'Missing "https://schema.space/metasat/frequency" from \ + "https://schema.space/metasat/uplink" value' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if isinstance(transmitter['uplink']['frequency'], int): + transmitter_data['type'] = 'Transceiver' + transmitter_data['uplink_low'] = transmitter['uplink']['frequency'] + else: + transmitter_data['type'] = 'Transponder' + if 'minimum' not in transmitter['uplink'][ + 'frequency'] or 'maximum' not in transmitter['uplink']['frequency']: + data = 'Missing "https://schema.org/minimum" or \ + "https://schema.org/maximum" from \ + "https://schema.space/metasat/frequency" value of \ + "https://schema.space/metasat/uplink"' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + transmitter_data['uplink_low'] = transmitter['uplink']['frequency']['minimum'] + transmitter_data['uplink_high'] = transmitter['uplink']['frequency']['maximum'] + if 'mode' in transmitter['uplink']: + try: + transmitter_data['uplink_mode'] = Mode.objects.get( + name=transmitter['uplink']['mode'] + ).pk + except Mode.DoesNotExist: + data = 'Unknown Mode: {}'.format(transmitter['uplink']['mode']) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if 'drift' in transmitter['uplink']: + transmitter_data['uplink_drift'] = transmitter['uplink']['drift'] + else: + transmitter_data['type'] = 'Transmitter' + + if 'downlink' in transmitter: + if isinstance(transmitter['downlink'], list): + data = 'Multiple values for "https://schema.space/metasat/downlink" or \ + multiple entries with the same "@id" and different \ + "https://schema.space/metasat/downlink" values' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if 'frequency' not in transmitter['downlink']: + data = 'Missing "https://schema.space/metasat/frequency" from \ + "https://schema.space/metasat/downlink" value' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if isinstance(transmitter['downlink']['frequency'], + int) and not transmitter_data['type'] == 'Transponder': + transmitter_data['downlink_low'] = transmitter['downlink']['frequency'] + elif transmitter_data['type'] == 'Transponder': + if 'minimum' not in transmitter['downlink'][ + 'frequency'] or 'maximum' not in transmitter['downlink']['frequency']: + data = 'Missing "https://schema.org/minimum" or \ + "https://schema.org/maximum" from \ + "https://schema.space/metasat/frequency" value of \ + "https://schema.space/metasat/downlink"' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + transmitter_data['downlink_low'] = transmitter['downlink']['frequency'][ + 'minimum'] + transmitter_data['downlink_high'] = transmitter['downlink']['frequency'][ + 'maximum'] + else: + data = 'Expected integer for "https://schema.space/metasat/frequency" value \ + of "https://schema.space/metasat/downlink"' + + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if 'mode' in transmitter['downlink']: + try: + transmitter_data['downlink_mode'] = Mode.objects.get( + name=transmitter['downlink']['mode'] + ).pk + except Mode.DoesNotExist: + data = 'Unknown Mode: {}'.format(transmitter['downlink']['mode']) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + if 'drift' in transmitter['downlink']: + transmitter_data['downlink_drift'] = transmitter['downlink']['drift'] + transmitters_data.append(transmitter_data) + + serializer = serializers.TransmitterEntrySerializer( + data=transmitters_data, many=True, allow_empty=True + ) + if serializer.is_valid(): + serializer.save() + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(status=status.HTTP_201_CREATED) + class LatestTleSetViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 """ diff --git a/db/base/structured_data.py b/db/base/structured_data.py index 96a445d..412c3a4 100644 --- a/db/base/structured_data.py +++ b/db/base/structured_data.py @@ -131,6 +131,20 @@ class TransmitterStructuredData(StructuredData): "citation": "schema:citation", "service": "service" } + self.frame = { + "@context": self.context, + 'transmitter': { + "@requireAll": True, + "description": {}, + "status": {}, + "citation": {}, + "service": {}, + "satellite": { + "@requireAll": True, + "norad_cat_id": {} + }, + } + } structured_data = [] transmitter_id_domain = Site.objects.get_current().domain + '/transmitter/' satellite_id_domain = Site.objects.get_current().domain + '/satellite/'