Newer
Older

Jorrit Schaap
committed

Jorrit Schaap
committed
import logging
logger = logging.getLogger(__name__)
from django.http import HttpResponse, JsonResponse, Http404

Jorrit Schaap
committed
from rest_framework.response import Response as RestResponse
from rest_framework import status
from django.shortcuts import get_object_or_404, render, redirect

Jorrit Schaap
committed
from lofar.sas.tmss.tmss.tmssapp import models

Jorrit Schaap
committed
from lofar.common.json_utils import get_default_json_object_for_schema

Jorrit Schaap
committed
from lofar.common.datetimeutils import formatDatetime

Jorrit Schaap
committed
from lofar.common.util import single_line_with_single_spaces
from lofar.sas.tmss.tmss.tmssapp.adapters.parset import convert_to_parset
from lofar.sas.tmss.tmss.tmssapp.adapters.reports import create_cycle_report
from lofar.sas.tmss.tmss.tmssapp.tasks import create_scheduling_unit_draft_from_observing_strategy_template, create_task_blueprints_and_subtasks_from_scheduling_unit_draft, create_task_drafts_from_scheduling_unit_draft
from drf_yasg.utils import swagger_auto_schema
Jörn Künsemöller
committed
from drf_yasg.openapi import Parameter
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny
from rest_framework.decorators import authentication_classes, permission_classes
from django.apps import apps

Jorrit Schaap
committed
import re

Jorrit Schaap
committed
from lofar.sas.tmss.tmss.tmssapp.serializers import SchedulingUnitDraftSerializer, SchedulingUnitBlueprintSerializer

Jorrit Schaap
committed
from rest_framework.decorators import api_view
Jörn Künsemöller
committed
from datetime import datetime
import dateutil.parser
Jörn Künsemöller
committed
from astropy.coordinates import Angle
import astropy.units
Jörn Künsemöller
committed
from lofar.sas.tmss.tmss.tmssapp.conversions import local_sidereal_time_for_utc_and_station, local_sidereal_time_for_utc_and_longitude, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set, coordinates_timestamps_and_stations_to_target_transit

Jorrit Schaap
committed
# Note: Decorate with @api_view to get this picked up by Swagger

Jorrit Schaap
committed
def subtask_template_default_specification(request, subtask_template_pk:int):

Jorrit Schaap
committed
subtask_template = get_object_or_404(models.SubtaskTemplate, pk=subtask_template_pk)

Jorrit Schaap
committed
spec = get_default_json_object_for_schema(subtask_template.schema)
return JsonResponse(spec)

Jorrit Schaap
committed
def task_template_default_specification(request, task_template_pk:int):
task_template = get_object_or_404(models.TaskTemplate, pk=task_template_pk)
spec = get_default_json_object_for_schema(task_template.schema)
return JsonResponse(spec)
def subtask_parset(request, subtask_pk:int):

Jorrit Schaap
committed
subtask = get_object_or_404(models.Subtask, pk=subtask_pk)
parset = convert_to_parset(subtask)
return HttpResponse(str(parset), content_type='text/plain')
Jörn Künsemöller
committed
return render(request, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), '../frontend','tmss_webapp/build/index.html'))
#return render(request, "../../../frontend/frontend_poc/build/index.html")
Jörn Künsemöller
committed
@api_view(['DELETE'])
def revoke_token_deauth(request, *args, **kwargs):
token = request.META['HTTP_AUTHORIZATION'].split(" ")[1]
invalidate_token = Token.objects.filter(key=token)
invalidate_token.delete()
def task_specify_observation(request, pk=None):
task = get_object_or_404(models.TaskDraft, pk=pk)
return HttpResponse("response", content_type='text/plain')
Jörn Künsemöller
committed
def authentication_state(request):
if not request.user.is_authenticated:
return JsonResponse({'is_authenticated': False})
return JsonResponse({'is_authenticated': True,
Jörn Künsemöller
committed
'username': request.user.username,
'email': request.user.email,
'id': request.user.id})
Jörn Künsemöller
committed
# Allow everybody to GET our publicly available template-json-schema's
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(#method='GET',
responses={200: 'Get the JSON schema from the template with the requested <template>, <name> and <version>',
404: 'the schema with requested <template>, <name> and <version> is not available'},
operation_description="Get the JSON schema for the given <template> with the given <name> and <version> as application/json content response.")
#@api_view(['GET']) # todo: !! decorating this as api_view somehow breaks json ref resolution !! fix this and double url issue in urls.py, then use decorator here to include in Swagger
def get_template_json_schema(request, template:str, name:str, version:str):
template_model = apps.get_model("tmssapp", template)
template_instance = get_object_or_404(template_model, name=name, version=version)
schema = template_instance.schema
response = JsonResponse(schema, json_dumps_params={"indent":2})
# config Access-Control. Our schemas use $ref url's to other schemas, mainly pointing to our own common schemas with base definitions.
# We instruct the client to allow fetching those.
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
return response

Jorrit Schaap
committed

Jorrit Schaap
committed
# Allow everybody to GET our publicly available LTA SIP XSD (XML Schema Definition for the LTA SIP)
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(#method='GET',
responses={200: 'Get the LTA SIP XSD',
404: 'not available'},
operation_description="Get the LTA SIP XSD.")
#@api_view(['GET']) # todo: !! decorating this as api_view somehow breaks json ref resolution !! fix this and double url issue in urls.py, then use decorator here to include in Swagger
def get_lta_sip_xsd(request):
lta_sip_xsd_path = os.path.join(os.environ["LOFARROOT"], "share", "lta", "LTA-SIP.xsd")
with open(lta_sip_xsd_path, 'rt') as file:
xsd = file.read()
return HttpResponse(content=xsd, content_type='application/xml')
# Allow everybody to GET our publicly available station group lookups
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(#method='GET',
responses={200: 'A JSON object with two properties: group:<the_group_name>, stations:<the_list_of_stations>',
404: 'No such group or template available'},
operation_description="Get a JSON list of stations for the given <station_group> name the the group definitions in the common_schema_template given by <template_name> and <template_version>")
#@api_view(['GET']) # todo: fix double url issue in urls.py, then use decorator here to include in Swagger
def get_stations_in_group(request, template_name:str, template_version:str, station_group:str):
station_schema_template = get_object_or_404(models.CommonSchemaTemplate, name=template_name, version=template_version)
station_schema = station_schema_template.schema
if 'station_group' not in station_schema.get('definitions', {}):
raise Http404('The JSON schema in template %s version %s has no station_group definitions' % (template_name, template_version))
groups = station_schema['definitions']['station_group']['anyOf']
try:
selected_group = next(g for g in groups if g['title'].lower() == station_group.lower())
except StopIteration:
raise Http404('No station_group with name "%s" found in the JSON schema. template=%s version=%s' % (station_group, template_name, template_version))
stations = selected_group['properties']['stations']['enum'][0]
return JsonResponse({'group': station_group,
'stations': stations})
Jörn Künsemöller
committed
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
responses={200: 'An isoformat timestamp of the current UTC clock of the system'},
Jörn Künsemöller
committed
operation_description="Get the current system time in UTC")
@api_view(['GET'])
Jörn Künsemöller
committed
def utc(request):
return HttpResponse(datetime.utcnow().isoformat(), content_type='text/plain')
Jörn Künsemöller
committed
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
responses={200: 'The LST time in hms format at the given UTC time and station or longitude'},
Jörn Künsemöller
committed
operation_description="Get LST time for UTC time and station or longitude",
manual_parameters=[Parameter(name='station', required=False, type='string', in_='query',
description="A station names (defaults to CS002)"),
Parameter(name='timestamp', required=False, type='string', in_='query',
description="A timestamp in isoformat (defaults to utcnow)"),
Parameter(name='longitude', required=False, type='string', in_='query',
description="A longitude as float")
Jörn Künsemöller
committed
])
@api_view(['GET'])
Jörn Künsemöller
committed
def lst(request):
# Handling optional parameters via django paths in urls.py is a pain, we access them on the request directly instead.
timestamp = request.GET.get('timestamp', None)
station = request.GET.get('station', None)
longitude = request.GET.get('longitude', None)
# conversions
if timestamp:
timestamp = dateutil.parser.parse(timestamp) # isot to datetime
if longitude:
longitude = float(longitude)
if station:
lst_lon = local_sidereal_time_for_utc_and_station(timestamp, station)
elif longitude:
lst_lon = local_sidereal_time_for_utc_and_longitude(timestamp, longitude)
else:
# fall back to defaults
lst_lon = local_sidereal_time_for_utc_and_station(timestamp)
# todo: do we want to return a dict, so users can make sure their parameters were parsed correctly instead?

Jorrit Schaap
committed
return HttpResponse(str(lst_lon), content_type='text/plain')
Jörn Künsemöller
committed
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
responses={200: 'A JSON object with sunrise, sunset, day and night of the given stations at the given timestamps'},
operation_description="Get sunrise, sunset, day and night for stations and timestamps.\n\n"
"Example request: /api/util/sun_rise_and_set?stations=CS002,CS005×tamps=2020-05-01,2020-09-09T11-11-00",
Jörn Künsemöller
committed
manual_parameters=[Parameter(name='stations', required=False, type='string', in_='query',
description="comma-separated list of station names"),
Parameter(name='timestamps', required=False, type='string', in_='query',
description="comma-separated list of isoformat timestamps")])
@api_view(['GET'])
Jörn Künsemöller
committed
def get_sun_rise_and_set(request):
"""
returns sunrise and sunset at the given stations and timestamps, or today at LOFAR core if none specified.
example request: /api/util/sun_rise_and_set?stations=CS002,CS005×tamps=2020-05-01,2020-09-09T11-11-00
"""
timestamps = request.GET.get('timestamps', None)
stations = request.GET.get('stations', None)
if timestamps is None:
Jörn Künsemöller
committed
timestamps = (datetime.utcnow(),)

Jorrit Schaap
committed
else:
Jörn Künsemöller
committed
timestamps = timestamps.split(',')
timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps]) # isot to datetime
Jörn Künsemöller
committed
if stations is None:
Jörn Künsemöller
committed
stations = ("CS002",)
Jörn Künsemöller
committed
else:
Jörn Künsemöller
committed
stations = tuple(stations.split(','))

Jorrit Schaap
committed
Jörn Künsemöller
committed
return JsonResponse(timestamps_and_stations_to_sun_rise_and_set(timestamps, stations))

Jorrit Schaap
committed
Jörn Künsemöller
committed
Jörn Künsemöller
committed
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
Jörn Künsemöller
committed
responses={200: 'A JSON object with angular distances of the given sky coordinates from the given solar system bodies at the given timestamps (seen from LOFAR core)'},
operation_description="Get angular distances of the given sky coordinates from the given solar system bodies at all given timestamps. \n\n"
Jörn Künsemöller
committed
"Example request: /api/util/angular_separation?angle1=1&angle2=1×tamps=2020-01-01T15,2020-01-01T16",
Jörn Künsemöller
committed
manual_parameters=[Parameter(name='angle1', required=True, type='string', in_='query',
description="first angle of celectial coordinates as float, e.g. RA"),
Parameter(name='angle2', required=True, type='string', in_='query',
description="second angle of celectial coordinates as float, e.g. RA"),
Parameter(name='direction_type', required=False, type='string', in_='query',
description="direction_type of celectial coordinates as string, e.g. J2000"),
Jörn Künsemöller
committed
Parameter(name='timestamps', required=False, type='string', in_='query',
description="comma-separated list of isoformat timestamps"),
Parameter(name='bodies', required=False, type='string', in_='query',
description="comma-separated list of solar system bodies")])
@api_view(['GET'])
Jörn Künsemöller
committed
def get_angular_separation(request):
Jörn Künsemöller
committed
'''
returns angular distances of the given sky coordinates from the given astronomical objects at the given timestamps and stations
'''
timestamps = request.GET.get('timestamps', None)
Jörn Künsemöller
committed
angle1 = request.GET.get('angle1')
angle2 = request.GET.get('angle2')
direction_type = request.GET.get("direction_type", "J2000")
bodies = tuple(request.GET.get('bodies', "sun,moon,jupiter").split(','))
if angle1 is None or angle2 is None:
raise ValueError("Please provide celestial coordinates via 'angle1', 'angle2' (and optionally 'direction_type') properties.")
Jörn Künsemöller
committed
if timestamps is None:
Jörn Künsemöller
committed
timestamps = (datetime.utcnow(),)
Jörn Künsemöller
committed
else:
timestamps = timestamps.split(',')
timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps]) # isot to datetime
Jörn Künsemöller
committed
Jörn Künsemöller
committed
# calculate
sep_dict = coordinates_and_timestamps_to_separation_from_bodies(angle1=angle1, angle2=angle2, direction_type=direction_type, bodies=bodies, timestamps=timestamps)
new_sep_dict = {}
Jörn Künsemöller
committed
Jörn Künsemöller
committed
# serialize angles and datetimes for json response
for body, timestamps in sep_dict.items():
for timestamp, angle in timestamps.items():
new_sep_dict.setdefault(body, {})[timestamp.isoformat()] = angle.rad
return JsonResponse(new_sep_dict)
Jörn Künsemöller
committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
responses={200: 'A JSON object with rise and set times of the given coordinates above the provided horizon, for each given station and timestamp.'},
operation_description="Get rise and set times of the given coordinates above the provided horizon, for each given station and timestamp. \n\n"
"Example request: /api/util/target_rise_and_set?angle1=0.5&angle2=0.5×tamps=2020-01-01T15&horizon=0.3",
manual_parameters=[Parameter(name='angle1', required=True, type='string', in_='query',
description="first angle of celectial coordinates as float, e.g. RA"),
Parameter(name='angle2', required=True, type='string', in_='query',
description="second angle of celectial coordinates as float, e.g. RA"),
Parameter(name='direction_type', required=False, type='string', in_='query',
description="direction_type of celectial coordinates as string, e.g. J2000"),
Parameter(name='timestamps', required=False, type='string', in_='query',
description="comma-separated list of isoformat timestamps"),
Parameter(name='stations', required=False, type='string', in_='query',
description="comma-separated list of station names"),
Parameter(name='horizon', required=False, type='string', in_='query',
description="Elevation above horizon for which to return rise/set times as float")])
@api_view(['GET'])
def get_target_rise_and_set(request):
'''
returns rise and set times of the given coordinates above the provided horizon, for each given station and timestamp.
'''
timestamps = request.GET.get('timestamps', None)
angle1 = request.GET.get('angle1')
angle2 = request.GET.get('angle2')
direction_type = request.GET.get("direction_type", "J2000")
stations = tuple(request.GET.get('stations', "CS002").split(','))
horizon = request.GET.get('horizon', None)
if angle1 is None or angle2 is None:
raise ValueError("Please provide celestial coordinates via 'angle1', 'angle2' (and optionally 'direction_type') properties.")
if timestamps is None:
timestamps = (datetime.utcnow(),)
else:
timestamps = timestamps.split(',')
timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps]) # isot to datetime
if horizon is None:
horizon = Angle(0, unit=astropy.units.rad)
else:
horizon = Angle(horizon, unit=astropy.units.rad)
# calculate
rise_set_dict = coordinates_timestamps_and_stations_to_target_rise_and_set(angle1=angle1, angle2=angle2, direction_type=direction_type, angle_to_horizon=horizon, timestamps=timestamps, stations=stations)
return JsonResponse(rise_set_dict)
Jörn Künsemöller
committed
@api_view(['GET'])
def get_target_transit(request):
'''
returns transit times of the given coordinates for each given station and timestamp.
'''
timestamps = request.GET.get('timestamps', None)
angle1 = request.GET.get('angle1')
angle2 = request.GET.get('angle2')
direction_type = request.GET.get("direction_type", "J2000")
stations = tuple(request.GET.get('stations', "CS002").split(','))
if angle1 is None or angle2 is None:
raise ValueError("Please provide celestial coordinates via 'angle1', 'angle2' (and optionally 'direction_type') properties.")
if timestamps is None:
timestamps = (datetime.utcnow(),)
else:
timestamps = timestamps.split(',')
timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps]) # isot to datetime
# calculate
transit_dict = coordinates_timestamps_and_stations_to_target_transit(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=stations)
return JsonResponse(transit_dict)
@swagger_auto_schema(method='GET', responses={200: 'A JSON object with cycles information for reporting.'},
manual_parameters=[Parameter(name='cycles', required=True, type='array', in_='query',
items={'type': 'string'}, description="Cycles' primary keys."),
Parameter(name='start_time', required=False, type='string', in_='query',
description="A timestamp in isoformat"),
Parameter(name='stop_time', required=False, type='string', in_='query',
description="A timestamp in isoformat")])
@api_view(['GET'])
def get_cycles_report(request):
cycles = str(request.GET.get('cycles')).split(',')
start, stop = request.GET.get('start', None), request.GET.get('stop', None)
# Check date parameters
if start:
try:
start = dateutil.parser.parse(start, ignoretz=True)
except Exception:
return HttpResponse('Error: please specify an isoformat timestamp for start_time', status=400)
if stop:
try:
stop = dateutil.parser.parse(stop, ignoretz=True)
except Exception:
return HttpResponse('Error: please specify an isoformat timestamp for stop_time', status=400)
for c_pk in cycles:
c = get_object_or_404(models.Cycle, pk=c_pk)
if not start or start < c.start:
start = c.start
if not stop or stop > c.stop:
stop = c.stop

Mario Raciti
committed
try:
results[c_pk] = create_cycle_report(request, c, start, stop)

Mario Raciti
committed
except RuntimeError:
return HttpResponse('Error: workflowapp is not running. It is needed to retrieve some reporting information.', status=503)
return JsonResponse(results)

Jorrit Schaap
committed
@api_view(['POST'])
def submit_trigger(request):
trigger_doc = request.data
logger.info("Received trigger submission: %s", single_line_with_single_spaces(trigger_doc))
# check if doc is valid
trigger_template = get_object_or_404(models.CommonSchemaTemplate, name="triggers")
trigger_template.validate_document(trigger_doc)
# gather relevant objects from db
strategy_template = get_object_or_404(models.SchedulingUnitObservingStrategyTemplate, name=trigger_doc['scheduling_unit_observing_strategy_template']['name'], version=trigger_doc['scheduling_unit_observing_strategy_template']['version'])
scheduling_set = get_object_or_404(models.SchedulingSet, pk=trigger_doc['scheduling_set_id'])
# check permissions
if not scheduling_set.project.can_trigger:
msg = 'Project \'%s\' does not allow triggers' % scheduling_set.project
logger.error(msg)
return RestResponse(msg, status=status.HTTP_403_FORBIDDEN)

Jorrit Schaap
committed
from django.db import transaction
with transaction.atomic():
# try to create the draft from the trigger_doc
scheduling_unit_draft = create_scheduling_unit_draft_from_observing_strategy_template(strategy_template,
scheduling_set,
name=trigger_doc['name'],
description=trigger_doc.get('description'),
requirements_doc_overrides=trigger_doc['scheduling_unit_observing_strategy_template'].get('overrides', {}))
# indicate that we are allowed to interrupt the telescope
scheduling_unit_draft.interrupts_telescope = True
scheduling_unit_draft.save()
# instantiate the task_drafts
scheduling_unit_draft = create_task_drafts_from_scheduling_unit_draft(scheduling_unit_draft)

Jorrit Schaap
committed
# if the trigger mode is 'run', then turn it into a blueprint which the dynamic scheduler will try to pick up, given the scheduling constraints
if trigger_doc['mode'].lower() == 'run':
scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft)
return RestResponse(SchedulingUnitBlueprintSerializer(scheduling_unit_blueprint, context={'request': request}).data,
status=status.HTTP_201_CREATED)
return RestResponse(SchedulingUnitDraftSerializer(scheduling_unit_draft, context={'request': request}).data,

Jorrit Schaap
committed
status=status.HTTP_201_CREATED)