Skip to content
Snippets Groups Projects
views.py 23.4 KiB
Newer Older
import logging
logger = logging.getLogger(__name__)

from django.http import HttpResponse, JsonResponse, Http404
from rest_framework.response import Response as RestResponse
from rest_framework import status
from django.shortcuts import get_object_or_404, render, redirect
from lofar.sas.tmss.tmss.tmssapp import models
from lofar.common.json_utils import get_default_json_object_for_schema
from lofar.common.datetimeutils import formatDatetime
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
from rest_framework.authtoken.models import Token
from rest_framework.permissions import AllowAny
from rest_framework.decorators import authentication_classes, permission_classes
from lofar.sas.tmss.tmss.tmssapp.serializers import SchedulingUnitDraftSerializer, SchedulingUnitBlueprintSerializer
from rest_framework.decorators import api_view
from astropy.coordinates import Angle
import astropy.units
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
# Note: Decorate with @api_view to get this picked up by Swagger

def subtask_template_default_specification(request, subtask_template_pk:int):
    subtask_template = get_object_or_404(models.SubtaskTemplate, pk=subtask_template_pk)
    spec = get_default_json_object_for_schema(subtask_template.schema)
    return JsonResponse(spec)

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):
    subtask = get_object_or_404(models.Subtask, pk=subtask_pk)
    parset = convert_to_parset(subtask)
    return HttpResponse(str(parset), content_type='text/plain')
def index(request):
    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")
@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()
    return HttpResponse(status=204)
def task_specify_observation(request, pk=None):
    task = get_object_or_404(models.TaskDraft, pk=pk)
    return HttpResponse("response", content_type='text/plain')

def authentication_state(request):
    if not request.user.is_authenticated:
        return JsonResponse({'is_authenticated': False})

    return JsonResponse({'is_authenticated': True,
                         'username': request.user.username,
                         'email': request.user.email,
                         'id': request.user.id})
# Allow everybody to GET our publicly available template-json-schema's
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
                     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
# 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])
                     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})


@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
                     responses={200: 'An isoformat timestamp of the current UTC clock of the system'},
                     operation_description="Get the current system time in UTC")
def utc(request):
    return HttpResponse(datetime.utcnow().isoformat(), content_type='text/plain')

@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'},
                     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")
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?
    return HttpResponse(str(lst_lon), content_type='text/plain')


@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&timestamps=2020-05-01,2020-09-09T11-11-00",
                     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")])
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&timestamps=2020-05-01,2020-09-09T11-11-00
    """
    timestamps = request.GET.get('timestamps', None)
    stations = request.GET.get('stations', None)
    if timestamps is None:
        timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps])  #  isot to datetime
    return JsonResponse(timestamps_and_stations_to_sun_rise_and_set(timestamps, stations))
@permission_classes([AllowAny])
@authentication_classes([AllowAny])
@swagger_auto_schema(method='GET',
                     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"
                                           "Example request: /api/util/angular_separation?angle1=1&angle2=1&timestamps=2020-01-01T15,2020-01-01T16",
                     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='bodies', required=False, type='string', in_='query',
                                                  description="comma-separated list of solar system bodies")])
@api_view(['GET'])
    '''
    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)
    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.")
        timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps])  #  isot to datetime
    # 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 = {}
    # 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)



@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&timestamps=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)

@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
            results[c_pk] = create_cycle_report(request, c, start, stop)
        except RuntimeError:
            return HttpResponse('Error: workflowapp is not running. It is needed to retrieve some reporting information.', status=503)
    return JsonResponse(results)


@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)

    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)

        # 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,