Skip to content

views.py

MeasurementQuery

Bases: APIView

Source code in vast_pipeline/views.py
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
class MeasurementQuery(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    def get(
        self,
        request: Request,
        ra_deg: float,
        dec_deg: float,
        image_id: int,
        radius_deg: float,
    ) -> FileResponse:
        """Return a DS9/JS9 region file for all Measurement objects for a given cone search
        on an Image. Optionally highlight sources based on a Measurement or Source ID.

        Args:
            request: Django HTTPRequest. Supports 4 URL GET parameters:
                - selection_model: either "measurement" or "source" (defaults to "measurement").
                - selection_id: the id for the given `selection_model`.
                - run_id: (optional) only return measurements for sources with the given pipeline
                    run id (defaults to None).
                - no_forced: (optional) If true, exclude forced-photometry measurements (defaults
                    to False).
                Measurement objects that match the given selection criterion will be
                highlighted. e.g. ?selection_model=measurement&selection_id=100 will highlight
                the Measurement object with id=100. ?selection_model=source&selection_id=5
                will highlight all Measurement objects associated with the Source object with
                id=5.
            ra_deg: Cone search RA in decimal degrees.
            dec_deg: Cone search Dec in decimal degrees.
            image_id: Primary key (id) of the Image object to search.
            radius_deg: Cone search radius in decimal degrees.

        Returns:
            FileResponse: Django FileReponse containing a DS9/JS9 region file.
        """
        columns = ["id", "name", "ra", "dec", "bmaj", "bmin", "pa", "forced", "source", "source__name"]
        selection_model = request.GET.get("selection_model", "measurement")
        selection_id = request.GET.get("selection_id", None)
        run_id = request.GET.get("run_id", None)
        no_forced = request.GET.get("forced", False)

        # validate selection query params
        if selection_id is not None:
            if selection_model not in ("measurement", "source"):
                raise Http404("GET param selection_model must be either 'measurement' or 'source'.")
            if selection_model == "measurement":
                selection_attr = "id"
                selection_name = "name"
            else:
                selection_attr = selection_model
                selection_name = "source__name"
            try:
                selection_id = int(selection_id)
            except ValueError:
                raise Http404("GET param selection_id must be an integer.")

        measurements = (
            Measurement.objects.filter(image=image_id)
            .cone_search(ra_deg, dec_deg, radius_deg)
            .values(*columns, __name=F(selection_name))
        )
        if run_id:
            measurements = measurements.filter(source__run__id=run_id)
        if no_forced:
            measurements = measurements.filter(forced=False)
        measurement_region_file = io.StringIO()
        for meas in measurements:
            if selection_id is not None:
                color = "#FF0000" if meas[selection_attr] == selection_id else "#0000FF"
            shape = (
                f"ellipse({meas['ra']}d, {meas['dec']}d, {meas['bmaj']}\", {meas['bmin']}\", "
                f"{meas['pa']+90+180}d)"
            )
            properties: Dict[str, Any] = {
                "color": color,
                "data": {
                    "text": f"{selection_model} ID: {meas[selection_attr]}",
                    "link": reverse(f"vast_pipeline:{selection_model}_detail", args=[selection_id]),
                }
            }
            if meas["forced"]:
                properties.update(strokeDashArray=[3, 2])
            region = f"{shape} {json.dumps(properties)}\n"
            measurement_region_file.write(region)
        measurement_region_file.seek(0)
        f = io.BytesIO(bytes(measurement_region_file.read(), encoding="utf8"))
        response = FileResponse(
            f,
            as_attachment=False,
            filename=f"image-{image_id}_{ra_deg:.5f}_{dec_deg:+.5f}_radius-{radius_deg:.3f}.reg",
        )
        return response

get(request, ra_deg, dec_deg, image_id, radius_deg)

Return a DS9/JS9 region file for all Measurement objects for a given cone search on an Image. Optionally highlight sources based on a Measurement or Source ID.

Parameters:

Name Type Description Default
request Request

Django HTTPRequest. Supports 4 URL GET parameters: - selection_model: either "measurement" or "source" (defaults to "measurement"). - selection_id: the id for the given selection_model. - run_id: (optional) only return measurements for sources with the given pipeline run id (defaults to None). - no_forced: (optional) If true, exclude forced-photometry measurements (defaults to False). Measurement objects that match the given selection criterion will be highlighted. e.g. ?selection_model=measurement&selection_id=100 will highlight the Measurement object with id=100. ?selection_model=source&selection_id=5 will highlight all Measurement objects associated with the Source object with id=5.

required
ra_deg float

Cone search RA in decimal degrees.

required
dec_deg float

Cone search Dec in decimal degrees.

required
image_id int

Primary key (id) of the Image object to search.

required
radius_deg float

Cone search radius in decimal degrees.

required

Returns:

Name Type Description
FileResponse FileResponse

Django FileReponse containing a DS9/JS9 region file.

Source code in vast_pipeline/views.py
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
def get(
    self,
    request: Request,
    ra_deg: float,
    dec_deg: float,
    image_id: int,
    radius_deg: float,
) -> FileResponse:
    """Return a DS9/JS9 region file for all Measurement objects for a given cone search
    on an Image. Optionally highlight sources based on a Measurement or Source ID.

    Args:
        request: Django HTTPRequest. Supports 4 URL GET parameters:
            - selection_model: either "measurement" or "source" (defaults to "measurement").
            - selection_id: the id for the given `selection_model`.
            - run_id: (optional) only return measurements for sources with the given pipeline
                run id (defaults to None).
            - no_forced: (optional) If true, exclude forced-photometry measurements (defaults
                to False).
            Measurement objects that match the given selection criterion will be
            highlighted. e.g. ?selection_model=measurement&selection_id=100 will highlight
            the Measurement object with id=100. ?selection_model=source&selection_id=5
            will highlight all Measurement objects associated with the Source object with
            id=5.
        ra_deg: Cone search RA in decimal degrees.
        dec_deg: Cone search Dec in decimal degrees.
        image_id: Primary key (id) of the Image object to search.
        radius_deg: Cone search radius in decimal degrees.

    Returns:
        FileResponse: Django FileReponse containing a DS9/JS9 region file.
    """
    columns = ["id", "name", "ra", "dec", "bmaj", "bmin", "pa", "forced", "source", "source__name"]
    selection_model = request.GET.get("selection_model", "measurement")
    selection_id = request.GET.get("selection_id", None)
    run_id = request.GET.get("run_id", None)
    no_forced = request.GET.get("forced", False)

    # validate selection query params
    if selection_id is not None:
        if selection_model not in ("measurement", "source"):
            raise Http404("GET param selection_model must be either 'measurement' or 'source'.")
        if selection_model == "measurement":
            selection_attr = "id"
            selection_name = "name"
        else:
            selection_attr = selection_model
            selection_name = "source__name"
        try:
            selection_id = int(selection_id)
        except ValueError:
            raise Http404("GET param selection_id must be an integer.")

    measurements = (
        Measurement.objects.filter(image=image_id)
        .cone_search(ra_deg, dec_deg, radius_deg)
        .values(*columns, __name=F(selection_name))
    )
    if run_id:
        measurements = measurements.filter(source__run__id=run_id)
    if no_forced:
        measurements = measurements.filter(forced=False)
    measurement_region_file = io.StringIO()
    for meas in measurements:
        if selection_id is not None:
            color = "#FF0000" if meas[selection_attr] == selection_id else "#0000FF"
        shape = (
            f"ellipse({meas['ra']}d, {meas['dec']}d, {meas['bmaj']}\", {meas['bmin']}\", "
            f"{meas['pa']+90+180}d)"
        )
        properties: Dict[str, Any] = {
            "color": color,
            "data": {
                "text": f"{selection_model} ID: {meas[selection_attr]}",
                "link": reverse(f"vast_pipeline:{selection_model}_detail", args=[selection_id]),
            }
        }
        if meas["forced"]:
            properties.update(strokeDashArray=[3, 2])
        region = f"{shape} {json.dumps(properties)}\n"
        measurement_region_file.write(region)
    measurement_region_file.seek(0)
    f = io.BytesIO(bytes(measurement_region_file.read(), encoding="utf8"))
    response = FileResponse(
        f,
        as_attachment=False,
        filename=f"image-{image_id}_{ra_deg:.5f}_{dec_deg:+.5f}_radius-{radius_deg:.3f}.reg",
    )
    return response

RawImageListSet

Bases: ViewSet

Source code in vast_pipeline/views.py
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
class RawImageListSet(ViewSet):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @staticmethod
    def gen_title_data_tokens(list_of_paths):
        '''
        generate a dataframe with extra columns for HTML tags title
        and data-tokens to generate something like:
        <option title={{title}} data-tokens={{datatokens}}>{{path}}</option>
        this assume a path like this:
        EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A.EPOCH06x.I.selavy.components.txt
        For the following dataframe columns
                                                     path
        EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A...
                                                 title
        VAST_2118-06A.EPOCH06x.I.selavy.components.txt
                                               datatokens
        EPOCH06x VAST_2118-06A.EPOCH06x.I.selavy.compo...
        '''
        df = pd.DataFrame(list_of_paths, columns=['path'])
        df = df.sort_values('path')
        df['title'] = df['path'].str.split(pat=os.sep).str.get(-1)
        df['datatokens'] = (
            df['path'].str.split(pat=os.sep).str.get(0)
            .str.cat(df['title'], sep=' ')
        )

        return df.to_dict(orient='records')

    def list(self, request):
        # generate the folders path regex, e.g. /path/to/images/**/*.fits
        # first generate the list of main subfolders, e.g. [EPOCH01, ... ]
        img_root = settings.RAW_IMAGE_DIR
        if not os.path.exists(img_root):
            msg = 'Raw image folder does not exists'
            messages.error(request, msg)
            raise Http404(msg)

        img_subfolders_gen = filter(
            lambda x: os.path.isdir(os.path.join(img_root, x)),
            os.listdir(img_root)
        )
        img_subfolders1, img_subfolders2 = tee(img_subfolders_gen)
        img_regex_list = list(map(
            lambda x: os.path.join(img_root, x, '**' + os.sep + '*.fits'),
            img_subfolders1
        ))
        selavy_regex_list = list(map(
            lambda x: os.path.join(img_root, x, '**' + os.sep + '*.txt'),
            img_subfolders2
        ))
        # add home directory user data for user and jupyter-user (user = github name)
        req_user = request.user.username
        for user in [f'{req_user}', f'jupyter-{req_user}']:
            if settings.HOME_DATA_ROOT is not None:
                user_home_data = os.path.join(
                    settings.HOME_DATA_ROOT, user, settings.HOME_DATA_DIR
                )
            else:
                user_home_data = os.path.join(
                    os.path.expanduser(f'~{user}'), settings.HOME_DATA_DIR
                )
            if settings.HOME_DATA_DIR and os.path.exists(user_home_data):
                img_regex_list.append(os.path.join(user_home_data, '**' + os.sep + '*.fits'))
                selavy_regex_list.append(os.path.join(user_home_data, '**' + os.sep + '*.txt'))

        # generate raw image list in parallel
        dask_list = db.from_sequence(img_regex_list)
        fits_files = (
            dask_list.map(lambda x: glob(x, recursive=True))
            .flatten()
            .compute()
        )
        if not fits_files:
            messages.info(request, 'no fits files found')

        # generate raw image list in parallel
        dask_list = db.from_sequence(selavy_regex_list)
        selavy_files = (
            dask_list.map(lambda x: glob(x, recursive=True))
            .flatten()
            .compute()
        )
        if not fits_files:
            messages.info(request, 'no selavy files found')

        # generate response datastructure
        data = {
            'fits': self.gen_title_data_tokens(fits_files),
            'selavy': self.gen_title_data_tokens(selavy_files)
        }
        serializer = RawImageSelavyListSerializer(data)

        return Response(serializer.data)

gen_title_data_tokens(list_of_paths) staticmethod

generate a dataframe with extra columns for HTML tags title and data-tokens to generate something like:

this assume a path like this: EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A.EPOCH06x.I.selavy.components.txt For the following dataframe columns path EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A... title VAST_2118-06A.EPOCH06x.I.selavy.components.txt datatokens EPOCH06x VAST_2118-06A.EPOCH06x.I.selavy.compo...

Source code in vast_pipeline/views.py
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
@staticmethod
def gen_title_data_tokens(list_of_paths):
    '''
    generate a dataframe with extra columns for HTML tags title
    and data-tokens to generate something like:
    <option title={{title}} data-tokens={{datatokens}}>{{path}}</option>
    this assume a path like this:
    EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A.EPOCH06x.I.selavy.components.txt
    For the following dataframe columns
                                                 path
    EPOCH06x/COMBINED/STOKESI_SELAVY/VAST_2118-06A...
                                             title
    VAST_2118-06A.EPOCH06x.I.selavy.components.txt
                                           datatokens
    EPOCH06x VAST_2118-06A.EPOCH06x.I.selavy.compo...
    '''
    df = pd.DataFrame(list_of_paths, columns=['path'])
    df = df.sort_values('path')
    df['title'] = df['path'].str.split(pat=os.sep).str.get(-1)
    df['datatokens'] = (
        df['path'].str.split(pat=os.sep).str.get(0)
        .str.cat(df['title'], sep=' ')
    )

    return df.to_dict(orient='records')

RunViewSet

Bases: ModelViewSet

Source code in vast_pipeline/views.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
class RunViewSet(ModelViewSet):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]
    queryset = Run.objects.all()
    serializer_class = RunSerializer

    @rest_framework.decorators.action(detail=True, methods=['get'])
    def images(self, request, pk=None):
        qs = Image.objects.filter(run__id=pk).order_by('id')
        qs = self.filter_queryset(qs)
        page = self.paginate_queryset(qs)
        if page is not None:
            serializer = ImageSerializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = ImageSerializer(qs, many=True)
        return Response(serializer.data)

    @rest_framework.decorators.action(detail=True, methods=['get'])
    def measurements(self, request, pk=None):
        qs = Measurement.objects.filter(image__run__in=[pk]).order_by('id')
        qs = self.filter_queryset(qs)
        page = self.paginate_queryset(qs)
        if page is not None:
            serializer = MeasurementSerializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = MeasurementSerializer(qs, many=True)
        return Response(serializer.data)

    @rest_framework.decorators.action(detail=True, methods=['post'])
    def run(
        self, request: Request, pk: Optional[int] = None
    ) -> HttpResponseRedirect:
        """
        Launches a pipeline run using a Django Q cluster. Includes a check
        on ownership or admin stataus of the user to make sure processing
        is allowed.

        Args:
            request: Django REST Framework request object.
            pk: Run object primary key. Defaults to None.

        Raises:
            Http404: if a Source with the given `pk` cannot be found.

        Returns:
            Response: Returns to the orignal request page (the pipeline run
                detail).
        """
        if not pk:
            messages.error(
                request,
                'Error in config write: Run pk parameter null or not passed'
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            p_run = get_object_or_404(self.queryset, pk=pk)
        except Exception as e:
            messages.error(request, f'Error in config write: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # make sure that only the run creator or an admin can request the run
        # to be processed.
        if p_run.user != request.user and not request.user.is_staff:
            msg = 'You do not have permission to process this pipeline run!'
            messages.error(request, msg)
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # check that it's not already running or queued
        if p_run.status in ["RUN", "QUE", "RES"]:
            msg = (
                f'{p_run.name} is already running, queued or restoring.'
                ' Please wait for the run to complete before trying to'
                ' submit again.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        if Run.objects.check_max_runs(settings.MAX_PIPELINE_RUNS):
            msg = (
                'The maximum number of simultaneous pipeline runs has been'
                f' reached ({settings.MAX_PIPELINE_RUNS})! Please try again'
                ' when other jobs have finished.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        prev_status = p_run.status
        try:
            with transaction.atomic():
                p_run.status = 'QUE'
                p_run.save()

            debug_flag = True if request.POST.get('debug', None) else False
            full_rerun = True if request.POST.get('fullReRun', None) else False

            async_task(
                'vast_pipeline.management.commands.runpipeline.run_pipe',
                p_run.name, p_run.path, p_run, False, debug_flag,
                task_name=p_run.name, ack_failure=True, user=request.user,
                full_rerun=full_rerun, prev_ui_status=prev_status
            )
            msg = mark_safe(
                f'<b>{p_run.name}</b> successfully sent to the queue!<br><br>Refresh the'
                ' page to check the status.'
            )
            messages.success(
                request,
                msg
            )
        except Exception as e:
            with transaction.atomic():
                p_run.status = 'ERR'
                p_run.save()
            messages.error(request, f'Error in running pipeline: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        return HttpResponseRedirect(
            reverse('vast_pipeline:run_detail', args=[p_run.id])
        )

    @rest_framework.decorators.action(detail=True, methods=['post'])
    def restore(
        self, request: Request, pk: Optional[int] = None
    ) -> HttpResponseRedirect:
        """
        Launches a restore pipeline run using a Django Q cluster. Includes a
        check on ownership or admin status of the user to make sure
        processing is allowed.

        Args:
            request: Django REST Framework request object.
            pk: Run object primary key. Defaults to None.

        Raises:
            Http404: if a Source with the given `pk` cannot be found.

        Returns:
            Response: Returns to the orignal request page (the pipeline run
                detail).
        """
        if not pk:
            messages.error(
                request,
                'Error in config write: Run pk parameter null or not passed'
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            p_run = get_object_or_404(self.queryset, pk=pk)
        except Exception as e:
            messages.error(request, f'Error in run fetch: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # make sure that only the run creator or an admin can request the run
        # to be processed.
        if p_run.user != request.user and not request.user.is_staff:
            msg = 'You do not have permission to process this pipeline run!'
            messages.error(request, msg)
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # check that it's not already running or queued
        if p_run.status in ["RUN", "QUE", "RES", "INI"]:
            msg = (
                f'{p_run.name} is already running, queued, restoring or is '
                'only initialised. It cannot be restored at this time.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        if Run.objects.check_max_runs(settings.MAX_PIPELINE_RUNS):
            msg = (
                'The maximum number of simultaneous pipeline runs has been'
                f' reached ({settings.MAX_PIPELINE_RUNS})! Please try again'
                ' when other jobs have finished.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        prev_status = p_run.status
        try:
            debug_flag = 3 if request.POST.get('restoreDebug', None) else 1

            async_task(
                'django.core.management.call_command',
                'restorepiperun',
                p_run.path,
                no_confirm=True,
                verbosity=debug_flag
            )

            msg = mark_safe(
                f'Restore <b>{p_run.name}</b> successfully sent to the queue!<br><br>Refresh the'
                ' page to check the status.'
            )
            messages.success(
                request,
                msg
            )
        except Exception as e:
            with transaction.atomic():
                p_run.status = 'ERR'
                p_run.save()
            messages.error(request, f'Error in restoring run: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        return HttpResponseRedirect(
            reverse('vast_pipeline:run_detail', args=[p_run.id])
        )

    @rest_framework.decorators.action(detail=True, methods=['post'])
    def delete(
        self, request: Request, pk: Optional[int] = None
    ) -> HttpResponseRedirect:
        """
        Launches the remove pipeline run using a Django Q cluster. Includes a
        check on ownership or admin status of the user to make sure
        deletion is allowed.

        Args:
            request (Request): Django REST Framework request object.
            pk (int, optional): Run object primary key. Defaults to None.

        Raises:
            Http404: if a Source with the given `pk` cannot be found.

        Returns:
            Response: Returns to the run index page (list of pipeline runs).
        """
        if not pk:
            messages.error(
                request,
                'Error in config write: Run pk parameter null or not passed'
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            p_run = get_object_or_404(self.queryset, pk=pk)
        except Exception as e:
            messages.error(request, f'Error in run fetch: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # make sure that only the run creator or an admin can request the run
        # to be processed.
        if p_run.user != request.user and not request.user.is_staff:
            msg = 'You do not have permission to process this pipeline run!'
            messages.error(request, msg)
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # check that it's not already running or queued
        if p_run.status in ["RUN", "QUE", "RES"]:
            msg = (
                f'{p_run.name} is already running, queued or restoring. '
                'It cannot be deleted at this time.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            async_task(
                'django.core.management.call_command',
                'clearpiperun',
                p_run.path,
                remove_all=True,
            )

            msg = mark_safe(
                f'Delete <b>{p_run.name}</b> successfully requested!<br><br>'
                ' Refresh the Pipeline Runs page for the deletion to take effect.'
            )
            messages.success(
                request,
                msg
            )
        except Exception as e:
            with transaction.atomic():
                p_run.status = 'ERR'
                p_run.save()
            messages.error(request, f'Error in deleting run: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        return HttpResponseRedirect(
            reverse('vast_pipeline:run_index')
        )

    @rest_framework.decorators.action(detail=True, methods=['post'])
    def genarrow(
        self, request: Request, pk: Optional[int] = None
    ) ->HttpResponseRedirect:
        """
        Launches the create arrow files process for a pipeline run using
        a Django Q cluster. Includes a check on ownership or admin status of
        the user to make sure the creation is allowed.

        Args:
            request: Django REST Framework request object.
            pk: Run object primary key. Defaults to None.

        Raises:
            Http404: if a Source with the given `pk` cannot be found.

        Returns:
            Response: Returns to the orignal request page (the pipeline run
                detail).
        """
        if not pk:
            messages.error(
                request,
                'Error in config write: Run pk parameter null or not passed'
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            p_run = get_object_or_404(self.queryset, pk=pk)
        except Exception as e:
            messages.error(request, f'Error in run fetch: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # make sure that only the run creator or an admin can request the run
        # to be processed.
        if p_run.user != request.user and not request.user.is_staff:
            msg = 'You do not have permission to process this pipeline run!'
            messages.error(request, msg)
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        # check that it's not already running or queued
        if p_run.status != "END":
            msg = (
                f'{p_run.name} has not completed successfully.'
                ' The arrow files can only be generated after the run is'
                ' successful.'
            )
            messages.error(
                request,
                msg
            )
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        try:
            overwrite_flag = True if request.POST.get('arrowOverwrite', None) else False

            async_task(
                'django.core.management.call_command',
                'createmeasarrow',
                p_run.path,
                overwrite=overwrite_flag,
                verbosity=3
            )

            msg = mark_safe(
                f'Generate the arrow files for <b>{p_run.name}</b> successfully requested!<br><br>'
                ' Refresh the page and check the generate arrow log output for the status of the process.'
            )
            messages.success(
                request,
                msg
            )
        except Exception as e:
            with transaction.atomic():
                p_run.status = 'ERR'
                p_run.save()
            messages.error(request, f'Error in deleting run: {e}')
            return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

        return HttpResponseRedirect(
            reverse('vast_pipeline:run_detail', args=[p_run.id])
        )

delete(request, pk=None)

Launches the remove pipeline run using a Django Q cluster. Includes a check on ownership or admin status of the user to make sure deletion is allowed.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk int

Run object primary key. Defaults to None.

None

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Name Type Description
Response HttpResponseRedirect

Returns to the run index page (list of pipeline runs).

Source code in vast_pipeline/views.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
@rest_framework.decorators.action(detail=True, methods=['post'])
def delete(
    self, request: Request, pk: Optional[int] = None
) -> HttpResponseRedirect:
    """
    Launches the remove pipeline run using a Django Q cluster. Includes a
    check on ownership or admin status of the user to make sure
    deletion is allowed.

    Args:
        request (Request): Django REST Framework request object.
        pk (int, optional): Run object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        Response: Returns to the run index page (list of pipeline runs).
    """
    if not pk:
        messages.error(
            request,
            'Error in config write: Run pk parameter null or not passed'
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        p_run = get_object_or_404(self.queryset, pk=pk)
    except Exception as e:
        messages.error(request, f'Error in run fetch: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # make sure that only the run creator or an admin can request the run
    # to be processed.
    if p_run.user != request.user and not request.user.is_staff:
        msg = 'You do not have permission to process this pipeline run!'
        messages.error(request, msg)
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # check that it's not already running or queued
    if p_run.status in ["RUN", "QUE", "RES"]:
        msg = (
            f'{p_run.name} is already running, queued or restoring. '
            'It cannot be deleted at this time.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        async_task(
            'django.core.management.call_command',
            'clearpiperun',
            p_run.path,
            remove_all=True,
        )

        msg = mark_safe(
            f'Delete <b>{p_run.name}</b> successfully requested!<br><br>'
            ' Refresh the Pipeline Runs page for the deletion to take effect.'
        )
        messages.success(
            request,
            msg
        )
    except Exception as e:
        with transaction.atomic():
            p_run.status = 'ERR'
            p_run.save()
        messages.error(request, f'Error in deleting run: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    return HttpResponseRedirect(
        reverse('vast_pipeline:run_index')
    )

genarrow(request, pk=None)

Launches the create arrow files process for a pipeline run using a Django Q cluster. Includes a check on ownership or admin status of the user to make sure the creation is allowed.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk Optional[int]

Run object primary key. Defaults to None.

None

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Name Type Description
Response HttpResponseRedirect

Returns to the orignal request page (the pipeline run detail).

Source code in vast_pipeline/views.py
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
@rest_framework.decorators.action(detail=True, methods=['post'])
def genarrow(
    self, request: Request, pk: Optional[int] = None
) ->HttpResponseRedirect:
    """
    Launches the create arrow files process for a pipeline run using
    a Django Q cluster. Includes a check on ownership or admin status of
    the user to make sure the creation is allowed.

    Args:
        request: Django REST Framework request object.
        pk: Run object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        Response: Returns to the orignal request page (the pipeline run
            detail).
    """
    if not pk:
        messages.error(
            request,
            'Error in config write: Run pk parameter null or not passed'
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        p_run = get_object_or_404(self.queryset, pk=pk)
    except Exception as e:
        messages.error(request, f'Error in run fetch: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # make sure that only the run creator or an admin can request the run
    # to be processed.
    if p_run.user != request.user and not request.user.is_staff:
        msg = 'You do not have permission to process this pipeline run!'
        messages.error(request, msg)
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # check that it's not already running or queued
    if p_run.status != "END":
        msg = (
            f'{p_run.name} has not completed successfully.'
            ' The arrow files can only be generated after the run is'
            ' successful.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        overwrite_flag = True if request.POST.get('arrowOverwrite', None) else False

        async_task(
            'django.core.management.call_command',
            'createmeasarrow',
            p_run.path,
            overwrite=overwrite_flag,
            verbosity=3
        )

        msg = mark_safe(
            f'Generate the arrow files for <b>{p_run.name}</b> successfully requested!<br><br>'
            ' Refresh the page and check the generate arrow log output for the status of the process.'
        )
        messages.success(
            request,
            msg
        )
    except Exception as e:
        with transaction.atomic():
            p_run.status = 'ERR'
            p_run.save()
        messages.error(request, f'Error in deleting run: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    return HttpResponseRedirect(
        reverse('vast_pipeline:run_detail', args=[p_run.id])
    )

restore(request, pk=None)

Launches a restore pipeline run using a Django Q cluster. Includes a check on ownership or admin status of the user to make sure processing is allowed.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk Optional[int]

Run object primary key. Defaults to None.

None

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Name Type Description
Response HttpResponseRedirect

Returns to the orignal request page (the pipeline run detail).

Source code in vast_pipeline/views.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
@rest_framework.decorators.action(detail=True, methods=['post'])
def restore(
    self, request: Request, pk: Optional[int] = None
) -> HttpResponseRedirect:
    """
    Launches a restore pipeline run using a Django Q cluster. Includes a
    check on ownership or admin status of the user to make sure
    processing is allowed.

    Args:
        request: Django REST Framework request object.
        pk: Run object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        Response: Returns to the orignal request page (the pipeline run
            detail).
    """
    if not pk:
        messages.error(
            request,
            'Error in config write: Run pk parameter null or not passed'
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        p_run = get_object_or_404(self.queryset, pk=pk)
    except Exception as e:
        messages.error(request, f'Error in run fetch: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # make sure that only the run creator or an admin can request the run
    # to be processed.
    if p_run.user != request.user and not request.user.is_staff:
        msg = 'You do not have permission to process this pipeline run!'
        messages.error(request, msg)
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # check that it's not already running or queued
    if p_run.status in ["RUN", "QUE", "RES", "INI"]:
        msg = (
            f'{p_run.name} is already running, queued, restoring or is '
            'only initialised. It cannot be restored at this time.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    if Run.objects.check_max_runs(settings.MAX_PIPELINE_RUNS):
        msg = (
            'The maximum number of simultaneous pipeline runs has been'
            f' reached ({settings.MAX_PIPELINE_RUNS})! Please try again'
            ' when other jobs have finished.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    prev_status = p_run.status
    try:
        debug_flag = 3 if request.POST.get('restoreDebug', None) else 1

        async_task(
            'django.core.management.call_command',
            'restorepiperun',
            p_run.path,
            no_confirm=True,
            verbosity=debug_flag
        )

        msg = mark_safe(
            f'Restore <b>{p_run.name}</b> successfully sent to the queue!<br><br>Refresh the'
            ' page to check the status.'
        )
        messages.success(
            request,
            msg
        )
    except Exception as e:
        with transaction.atomic():
            p_run.status = 'ERR'
            p_run.save()
        messages.error(request, f'Error in restoring run: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    return HttpResponseRedirect(
        reverse('vast_pipeline:run_detail', args=[p_run.id])
    )

run(request, pk=None)

Launches a pipeline run using a Django Q cluster. Includes a check on ownership or admin stataus of the user to make sure processing is allowed.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk Optional[int]

Run object primary key. Defaults to None.

None

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Name Type Description
Response HttpResponseRedirect

Returns to the orignal request page (the pipeline run detail).

Source code in vast_pipeline/views.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
@rest_framework.decorators.action(detail=True, methods=['post'])
def run(
    self, request: Request, pk: Optional[int] = None
) -> HttpResponseRedirect:
    """
    Launches a pipeline run using a Django Q cluster. Includes a check
    on ownership or admin stataus of the user to make sure processing
    is allowed.

    Args:
        request: Django REST Framework request object.
        pk: Run object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        Response: Returns to the orignal request page (the pipeline run
            detail).
    """
    if not pk:
        messages.error(
            request,
            'Error in config write: Run pk parameter null or not passed'
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    try:
        p_run = get_object_or_404(self.queryset, pk=pk)
    except Exception as e:
        messages.error(request, f'Error in config write: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # make sure that only the run creator or an admin can request the run
    # to be processed.
    if p_run.user != request.user and not request.user.is_staff:
        msg = 'You do not have permission to process this pipeline run!'
        messages.error(request, msg)
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    # check that it's not already running or queued
    if p_run.status in ["RUN", "QUE", "RES"]:
        msg = (
            f'{p_run.name} is already running, queued or restoring.'
            ' Please wait for the run to complete before trying to'
            ' submit again.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    if Run.objects.check_max_runs(settings.MAX_PIPELINE_RUNS):
        msg = (
            'The maximum number of simultaneous pipeline runs has been'
            f' reached ({settings.MAX_PIPELINE_RUNS})! Please try again'
            ' when other jobs have finished.'
        )
        messages.error(
            request,
            msg
        )
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    prev_status = p_run.status
    try:
        with transaction.atomic():
            p_run.status = 'QUE'
            p_run.save()

        debug_flag = True if request.POST.get('debug', None) else False
        full_rerun = True if request.POST.get('fullReRun', None) else False

        async_task(
            'vast_pipeline.management.commands.runpipeline.run_pipe',
            p_run.name, p_run.path, p_run, False, debug_flag,
            task_name=p_run.name, ack_failure=True, user=request.user,
            full_rerun=full_rerun, prev_ui_status=prev_status
        )
        msg = mark_safe(
            f'<b>{p_run.name}</b> successfully sent to the queue!<br><br>Refresh the'
            ' page to check the status.'
        )
        messages.success(
            request,
            msg
        )
    except Exception as e:
        with transaction.atomic():
            p_run.status = 'ERR'
            p_run.save()
        messages.error(request, f'Error in running pipeline: {e}')
        return HttpResponseRedirect(request.META.get('HTTP_REFERER'))

    return HttpResponseRedirect(
        reverse('vast_pipeline:run_detail', args=[p_run.id])
    )

SourcePlotsSet

Bases: ViewSet

Source code in vast_pipeline/views.py
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
class SourcePlotsSet(ViewSet):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @rest_framework.decorators.action(methods=['get'], detail=True)
    def lightcurve(self, request: Request, pk: int = None) -> Response:
        """Create lightcurve and 2-epoch metric graph plots for a source.

        Args:
            request (Request): Django REST Framework request object.
            pk (int, optional): Source object primary key. Defaults to None.

        Raises:
            Http404: if a Source with the given `pk` cannot be found.

        Returns:
            Response: Django REST Framework response object containing the Bokeh plot in
                JSON format to be embedded in the HTML template.
        """
        try:
            source = Source.objects.get(pk=pk)
        except Source.DoesNotExist:
            raise Http404
        # TODO raster plots version for Slack posts
        use_peak_flux = request.query_params.get("peak_flux", "true").lower() == "true"
        plot_document = plot_lightcurve(source, use_peak_flux=use_peak_flux)
        return Response(json_item(plot_document))

    @rest_framework.decorators.action(methods=['get'], detail=False)
    def etavplot(self, request: Request) -> Response:
        """Create the eta-V plot.

        Args:
            request (Request): Django REST Framework request object.

        Raises:
            Http404: if no sources are found.

        Returns:
            Response: Django REST Framework response object containing the Bokeh plot in
                JSON format to be embedded in the HTML template.
        """
        source_query_result_id_list = request.session.get("source_query_result_ids", [])
        try:
            source = Source.objects.filter(pk__in=source_query_result_id_list)
        except Source.DoesNotExist:
            raise Http404
        # TODO raster plots version for Slack posts
        use_peak_flux = request.query_params.get("peak_flux", "true").lower() == "true"

        eta_sigma = float(request.query_params.get("eta_sigma", 3.0))
        v_sigma = float(request.query_params.get("v_sigma", 3.0))

        plot_document = plot_eta_v_bokeh(
            source, eta_sigma=eta_sigma, v_sigma=v_sigma, use_peak_flux=use_peak_flux)
        return Response(json_item(plot_document))

etavplot(request)

Create the eta-V plot.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required

Raises:

Type Description
Http404

if no sources are found.

Returns:

Name Type Description
Response Response

Django REST Framework response object containing the Bokeh plot in JSON format to be embedded in the HTML template.

Source code in vast_pipeline/views.py
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
@rest_framework.decorators.action(methods=['get'], detail=False)
def etavplot(self, request: Request) -> Response:
    """Create the eta-V plot.

    Args:
        request (Request): Django REST Framework request object.

    Raises:
        Http404: if no sources are found.

    Returns:
        Response: Django REST Framework response object containing the Bokeh plot in
            JSON format to be embedded in the HTML template.
    """
    source_query_result_id_list = request.session.get("source_query_result_ids", [])
    try:
        source = Source.objects.filter(pk__in=source_query_result_id_list)
    except Source.DoesNotExist:
        raise Http404
    # TODO raster plots version for Slack posts
    use_peak_flux = request.query_params.get("peak_flux", "true").lower() == "true"

    eta_sigma = float(request.query_params.get("eta_sigma", 3.0))
    v_sigma = float(request.query_params.get("v_sigma", 3.0))

    plot_document = plot_eta_v_bokeh(
        source, eta_sigma=eta_sigma, v_sigma=v_sigma, use_peak_flux=use_peak_flux)
    return Response(json_item(plot_document))

lightcurve(request, pk=None)

Create lightcurve and 2-epoch metric graph plots for a source.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk int

Source object primary key. Defaults to None.

None

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Name Type Description
Response Response

Django REST Framework response object containing the Bokeh plot in JSON format to be embedded in the HTML template.

Source code in vast_pipeline/views.py
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
@rest_framework.decorators.action(methods=['get'], detail=True)
def lightcurve(self, request: Request, pk: int = None) -> Response:
    """Create lightcurve and 2-epoch metric graph plots for a source.

    Args:
        request (Request): Django REST Framework request object.
        pk (int, optional): Source object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        Response: Django REST Framework response object containing the Bokeh plot in
            JSON format to be embedded in the HTML template.
    """
    try:
        source = Source.objects.get(pk=pk)
    except Source.DoesNotExist:
        raise Http404
    # TODO raster plots version for Slack posts
    use_peak_flux = request.query_params.get("peak_flux", "true").lower() == "true"
    plot_document = plot_lightcurve(source, use_peak_flux=use_peak_flux)
    return Response(json_item(plot_document))

SourceViewSet

Bases: ModelViewSet

Source code in vast_pipeline/views.py
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
class SourceViewSet(ModelViewSet):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]
    serializer_class = SourceSerializer

    def get_queryset(self):
        qs = Source.objects.all().filter(run__status='END')

        radius_conversions = {
            "arcsec": 3600.,
            "arcmin": 60.,
            "deg": 1.
        }

        qry_dict = {}
        p_run = self.request.query_params.get('run')
        if p_run:
            qry_dict['run__name'] = p_run

        flux_qry_flds = [
            'avg_flux_int',
            'avg_flux_peak',
            'min_flux_peak',
            'max_flux_peak',
            'min_flux_int',
            'max_flux_int',
            'min_flux_peak_isl_ratio',
            'min_flux_int_isl_ratio',
            'v_int',
            'v_peak',
            'eta_int',
            'eta_peak',
            'vs_abs_significant_max_int',
            'vs_abs_significant_max_peak',
            'm_abs_significant_max_int',
            'm_abs_significant_max_peak',
            'n_meas',
            'n_meas_sel',
            'n_meas_forced',
            'n_rel',
            'new_high_sigma',
            'avg_compactness',
            'min_snr',
            'max_snr',
            'n_neighbour_dist',
            'source_selection',
            'source_selection_type'
        ]

        neighbour_unit = self.request.query_params.get('NeighbourUnit')

        for fld in flux_qry_flds:
            for limit in ['max', 'min']:
                val = self.request.query_params.get(limit + '_' + fld)
                if val:
                    ky = fld + '__lte' if limit == 'max' else fld + '__gte'
                    if fld == 'n_neighbour_dist':
                        val = float(val) / radius_conversions[neighbour_unit]
                    qry_dict[ky] = val

        measurements = self.request.query_params.get('meas')
        if measurements:
            qry_dict['measurements'] = measurements

        if 'source_selection' in self.request.query_params:
            selection_type = self.request.query_params['source_selection_type']
            selection: List[str] = (
                self.request.query_params['source_selection']
                .replace(" ", "")
                .replace("VAST", "")  # remove published source prefix if present
                .split(",")
            )
            if selection_type == 'name':
                qry_dict['name__in'] = selection
            else:
                try:
                    selection = [int(i) for i in selection]
                    qry_dict['id__in'] = selection
                except:
                    # this avoids an error on the check if the user has
                    # accidentally entered names with a 'id' selection type.
                    qry_dict['id'] = -1

        if 'newsrc' in self.request.query_params:
            qry_dict['new'] = True

        if 'no_siblings' in self.request.query_params:
            qry_dict['n_sibl'] = 0

        if 'tags_include' in self.request.query_params:
            qry_dict['tags'] = self.request.query_params['tags_include']

        if 'tags_exclude' in self.request.query_params:
            qs = qs.exclude(tags=self.request.query_params['tags_exclude'])

        if qry_dict:
            qs = qs.filter(**qry_dict)

        radius = self.request.query_params.get('radius')
        radiusUnit = self.request.query_params.get('radiusunit')
        coordsys = self.request.query_params.get('coordsys')
        coord_string = self.request.query_params.get('coord')
        wavg_ra, wavg_dec = None, None
        if coord_string:
            coord = parse_coord(coord_string, coord_frame=coordsys).transform_to("icrs")
            wavg_ra = coord.ra.deg
            wavg_dec = coord.dec.deg

        if None not in (wavg_ra, wavg_dec, radius):
            radius = float(radius) / radius_conversions[radiusUnit]
            qs = qs.cone_search(wavg_ra, wavg_dec, radius)

        return qs

    def list(self, request, *args, **kwargs):
        """Override the DRF ModelViewSet.list function to store the source ID order in the
        user session to retain the source order for source detail view next and previous
        button links. Then, call the original list function.
        """
        queryset = self.filter_queryset(self.get_queryset())
        self.request.session["source_query_result_ids"] = list(
            queryset.values_list("id", flat=True)
        )
        return super().list(request, *args, **kwargs)

    @rest_framework.decorators.action(detail=True, methods=['get'])
    def related(self, request, pk=None):
        qs = Source.objects.filter(related__id=pk).order_by('id')
        qs = self.filter_queryset(qs)
        page = self.paginate_queryset(qs)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(qs, many=True)
        return Response(serializer.data)

list(request, *args, **kwargs)

Override the DRF ModelViewSet.list function to store the source ID order in the user session to retain the source order for source detail view next and previous button links. Then, call the original list function.

Source code in vast_pipeline/views.py
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
def list(self, request, *args, **kwargs):
    """Override the DRF ModelViewSet.list function to store the source ID order in the
    user session to retain the source order for source detail view next and previous
    button links. Then, call the original list function.
    """
    queryset = self.filter_queryset(self.get_queryset())
    self.request.session["source_query_result_ids"] = list(
        queryset.values_list("id", flat=True)
    )
    return super().list(request, *args, **kwargs)

UtilitiesSet

Bases: ViewSet

Source code in vast_pipeline/views.py
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
class UtilitiesSet(ViewSet):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    @staticmethod
    def _external_search_error_handler(
        external_query_func,
        coord: SkyCoord,
        radius: Angle,
        service_name: str,
        request: Request,
    ) -> List[Dict[str, Any]]:
        try:
            results = external_query_func(coord, radius)
        except requests.HTTPError:
            messages.error(request, f"Unable to get {service_name} query results.")
            results = []
        return results

    @rest_framework.decorators.action(methods=['get'], detail=False)
    def sesame_search(self, request: Request) -> Response:
        """Query the Sesame name resolver service and return a coordinate.

        Args:
            request (Request): Django REST framework Request object with GET parameters:
                - object_name (str): Object name to query.
                - service (str, optional): Sesame service to query (all, simbad, ned, vizier).
                    Defaults to "all".

        Returns:
            A Django REST framework Response. Will return JSON with status code:
                - 400 if the query params fail validation (i.e. if an invalid Sesame service
                    or no object name is provided) or if the name resolution fails. Error
                    messages are returned as an array of strings under the relevant query
                    parameter key. e.g. {"object_name": ["This field may not be blank."]}.
                - 200 if successful. Response data contains the passed in query parameters and
                    the resolved coordinate as a sexagesimal string with units hourangle, deg
                    under the key `coord`.
        """
        object_name = request.query_params.get("object_name", "")
        service = request.query_params.get("service", "all")

        serializer = SesameResultSerializer(data=dict(object_name=object_name, service=service))
        serializer.is_valid(raise_exception=True)

        return Response(serializer.data)

    @rest_framework.decorators.action(methods=['get'], detail=False)
    def coordinate_validator(self, request: Request) -> Response:
        """Validate a coordinate string.

        Args:
            request (Request): Django REST framework Request object with GET parameters:
                - coord (str): the coordinate string to validate.
                - frame (str): the frame for the given coordinate string e.g. icrs, galactic.

        Returns:
            A Django REST framework Response. Will return JSON with status code:
                - 400 if the query params fail validation, i.e. if a frame unknown to Astropy
                    is given, or the coordinate string fails to parse. Error messages are
                    returned as an array of strings under the relevant query parameter key.
                    e.g. {"coord": ["This field may not be blank."]}.
                - 200 if the coordinate string successfully validates. No other data is returned.
        """
        coord_string = request.query_params.get("coord", "")
        frame = request.query_params.get("frame", "")

        serializer = CoordinateValidatorSerializer(data=dict(coord=coord_string, frame=frame))
        serializer.is_valid(raise_exception=True)
        return Response()

    @rest_framework.decorators.action(methods=["get"], detail=False)
    def external_search(self, request: Request) -> Response:
        """Perform a cone search with external providers (e.g. SIMBAD, NED, TNS) and
        return the combined results.

        Args:
            request (Request): Django REST Framework Request object get GET parameters:
                - coord (str): the coordinate string to validate. Interpreted by
                    `astropy.coordiantes.SkyCoord`.
                - radius (str): the cone search radius with unit, e.g. "1arcmin".
                    Interpreted by `astropy.coordinates.Angle`

        Raises:
            serializers.ValidationError: if either the coordinate or radius parameters
                cannot be interpreted by `astropy.coordiantes.SkyCoord` or
                `astropy.coordinates.Angle`, respectively.

        Returns:
            A Django REST framework Response containing result records as a list
                under the data object key. Each record contains the properties:
                    - object_name: the name of the astronomical object.
                    - database: the source of the result, e.g. SIMBAD or NED.
                    - separation_arcsec: separation to the query coordinate in arcsec.
                    - otype: object type.
                    - otype_long: long form of the object type (only available for SIMBAD).
                    - ra_hms: RA coordinate string in <HH>h<MM>m<SS.SSS>s format.
                    - dec_dms: Dec coordinate string in ±<DD>d<MM>m<SS.SSS>s format.
        """
        coord_string = request.query_params.get("coord", "")
        radius_string = request.query_params.get("radius", "1arcmin")

        # validate inputs
        try:
            coord = parse_coord(coord_string)
        except ValueError as e:
            raise serializers.ValidationError({"coord": str(e.args[0])})

        try:
            radius = Angle(radius_string)
        except ValueError as e:
            raise serializers.ValidationError({"radius": str(e.args[0])})

        simbad_results = self._external_search_error_handler(
            external_query.simbad, coord, radius, "SIMBAD", request
        )
        ned_results = self._external_search_error_handler(
            external_query.ned, coord, radius, "NED", request
        )
        tns_results = self._external_search_error_handler(
            external_query.tns, coord, radius, "TNS", request
        )

        results = simbad_results + ned_results + tns_results
        serializer = ExternalSearchSerializer(data=results, many=True)
        serializer.is_valid(raise_exception=True)
        return Response(serializer.data)

coordinate_validator(request)

Validate a coordinate string.

Parameters:

Name Type Description Default
request Request

Django REST framework Request object with GET parameters: - coord (str): the coordinate string to validate. - frame (str): the frame for the given coordinate string e.g. icrs, galactic.

required

Returns:

Type Description
Response

A Django REST framework Response. Will return JSON with status code: - 400 if the query params fail validation, i.e. if a frame unknown to Astropy is given, or the coordinate string fails to parse. Error messages are returned as an array of strings under the relevant query parameter key. e.g. {"coord": ["This field may not be blank."]}. - 200 if the coordinate string successfully validates. No other data is returned.

Source code in vast_pipeline/views.py
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
@rest_framework.decorators.action(methods=['get'], detail=False)
def coordinate_validator(self, request: Request) -> Response:
    """Validate a coordinate string.

    Args:
        request (Request): Django REST framework Request object with GET parameters:
            - coord (str): the coordinate string to validate.
            - frame (str): the frame for the given coordinate string e.g. icrs, galactic.

    Returns:
        A Django REST framework Response. Will return JSON with status code:
            - 400 if the query params fail validation, i.e. if a frame unknown to Astropy
                is given, or the coordinate string fails to parse. Error messages are
                returned as an array of strings under the relevant query parameter key.
                e.g. {"coord": ["This field may not be blank."]}.
            - 200 if the coordinate string successfully validates. No other data is returned.
    """
    coord_string = request.query_params.get("coord", "")
    frame = request.query_params.get("frame", "")

    serializer = CoordinateValidatorSerializer(data=dict(coord=coord_string, frame=frame))
    serializer.is_valid(raise_exception=True)
    return Response()

Perform a cone search with external providers (e.g. SIMBAD, NED, TNS) and return the combined results.

Parameters:

Name Type Description Default
request Request

Django REST Framework Request object get GET parameters: - coord (str): the coordinate string to validate. Interpreted by astropy.coordiantes.SkyCoord. - radius (str): the cone search radius with unit, e.g. "1arcmin". Interpreted by astropy.coordinates.Angle

required

Raises:

Type Description
ValidationError

if either the coordinate or radius parameters cannot be interpreted by astropy.coordiantes.SkyCoord or astropy.coordinates.Angle, respectively.

Returns:

Type Description
Response

A Django REST framework Response containing result records as a list under the data object key. Each record contains the properties: - object_name: the name of the astronomical object. - database: the source of the result, e.g. SIMBAD or NED. - separation_arcsec: separation to the query coordinate in arcsec. - otype: object type. - otype_long: long form of the object type (only available for SIMBAD). - ra_hms: RA coordinate string in hms format. - dec_dms: Dec coordinate string in ±

dms format.

Source code in vast_pipeline/views.py
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
@rest_framework.decorators.action(methods=["get"], detail=False)
def external_search(self, request: Request) -> Response:
    """Perform a cone search with external providers (e.g. SIMBAD, NED, TNS) and
    return the combined results.

    Args:
        request (Request): Django REST Framework Request object get GET parameters:
            - coord (str): the coordinate string to validate. Interpreted by
                `astropy.coordiantes.SkyCoord`.
            - radius (str): the cone search radius with unit, e.g. "1arcmin".
                Interpreted by `astropy.coordinates.Angle`

    Raises:
        serializers.ValidationError: if either the coordinate or radius parameters
            cannot be interpreted by `astropy.coordiantes.SkyCoord` or
            `astropy.coordinates.Angle`, respectively.

    Returns:
        A Django REST framework Response containing result records as a list
            under the data object key. Each record contains the properties:
                - object_name: the name of the astronomical object.
                - database: the source of the result, e.g. SIMBAD or NED.
                - separation_arcsec: separation to the query coordinate in arcsec.
                - otype: object type.
                - otype_long: long form of the object type (only available for SIMBAD).
                - ra_hms: RA coordinate string in <HH>h<MM>m<SS.SSS>s format.
                - dec_dms: Dec coordinate string in ±<DD>d<MM>m<SS.SSS>s format.
    """
    coord_string = request.query_params.get("coord", "")
    radius_string = request.query_params.get("radius", "1arcmin")

    # validate inputs
    try:
        coord = parse_coord(coord_string)
    except ValueError as e:
        raise serializers.ValidationError({"coord": str(e.args[0])})

    try:
        radius = Angle(radius_string)
    except ValueError as e:
        raise serializers.ValidationError({"radius": str(e.args[0])})

    simbad_results = self._external_search_error_handler(
        external_query.simbad, coord, radius, "SIMBAD", request
    )
    ned_results = self._external_search_error_handler(
        external_query.ned, coord, radius, "NED", request
    )
    tns_results = self._external_search_error_handler(
        external_query.tns, coord, radius, "TNS", request
    )

    results = simbad_results + ned_results + tns_results
    serializer = ExternalSearchSerializer(data=results, many=True)
    serializer.is_valid(raise_exception=True)
    return Response(serializer.data)

Query the Sesame name resolver service and return a coordinate.

Parameters:

Name Type Description Default
request Request

Django REST framework Request object with GET parameters: - object_name (str): Object name to query. - service (str, optional): Sesame service to query (all, simbad, ned, vizier). Defaults to "all".

required

Returns:

Type Description
Response

A Django REST framework Response. Will return JSON with status code: - 400 if the query params fail validation (i.e. if an invalid Sesame service or no object name is provided) or if the name resolution fails. Error messages are returned as an array of strings under the relevant query parameter key. e.g. {"object_name": ["This field may not be blank."]}. - 200 if successful. Response data contains the passed in query parameters and the resolved coordinate as a sexagesimal string with units hourangle, deg under the key coord.

Source code in vast_pipeline/views.py
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
@rest_framework.decorators.action(methods=['get'], detail=False)
def sesame_search(self, request: Request) -> Response:
    """Query the Sesame name resolver service and return a coordinate.

    Args:
        request (Request): Django REST framework Request object with GET parameters:
            - object_name (str): Object name to query.
            - service (str, optional): Sesame service to query (all, simbad, ned, vizier).
                Defaults to "all".

    Returns:
        A Django REST framework Response. Will return JSON with status code:
            - 400 if the query params fail validation (i.e. if an invalid Sesame service
                or no object name is provided) or if the name resolution fails. Error
                messages are returned as an array of strings under the relevant query
                parameter key. e.g. {"object_name": ["This field may not be blank."]}.
            - 200 if successful. Response data contains the passed in query parameters and
                the resolved coordinate as a sexagesimal string with units hourangle, deg
                under the key `coord`.
    """
    object_name = request.query_params.get("object_name", "")
    service = request.query_params.get("service", "all")

    serializer = SesameResultSerializer(data=dict(object_name=object_name, service=service))
    serializer.is_valid(raise_exception=True)

    return Response(serializer.data)

SourceEtaVPlot(request)

The view for the main eta-V plot page.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required

Returns:

Type Description
Response

The response for the main eta-V page.

Source code in vast_pipeline/views.py
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
@login_required
def SourceEtaVPlot(request: Request) -> Response:
    """The view for the main eta-V plot page.

    Args:
        request (Request): Django REST Framework request object.

    Returns:
        The response for the main eta-V page.
    """
    min_sources = 50

    source_query_result_id_list = request.session.get("source_query_result_ids", [])

    sources_query_len = len(source_query_result_id_list)

    if sources_query_len < min_sources:
        messages.error(
            request,
            (
                f'The query has returned only {sources_query_len} sources.'
                f' A minimum of {min_sources} sources must be used to produce'
                ' the plot.'
            )
        )

        plot_ok = 0

    else:
        sources = Source.objects.filter(
            id__in=source_query_result_id_list,
            n_meas__gt=1,
            eta_peak__gt=0,
            eta_int__gt=0,
            v_peak__gt=0,
            v_int__gt=0
        )

        new_sources_ids_list = list(sources.values_list("id", flat=True))

        new_sources_query_len = len(new_sources_ids_list)

        diff = sources_query_len - new_sources_query_len

        if diff > 0:
            messages.warning(
                request,
                (
                    f'Removed {diff} sources that either had'
                    ' only one datapoint, or, an \u03B7 or V value of 0.'
                    ' Change the query options to avoid these sources.'
                )
            )

            request.session["source_query_result_ids"] = new_sources_ids_list

        if new_sources_query_len < min_sources:
            messages.error(
                request,
                (
                    'After filtering, the query has returned only'
                    f' {sources_query_len} sources. A minimum of {min_sources}'
                    ' sources must be used to produce the plot.'
                )
            )

            plot_ok = 0

        else:
            if new_sources_query_len > settings.ETA_V_DATASHADER_THRESHOLD:
                messages.info(
                    request,
                    (
                        "Sources outside of the selected sigma area"
                        " are displayed as a non-interactive averaged"
                        " distribution."
                    )
                )

            plot_ok = 1

    context = {
        'plot_ok': plot_ok,
    }

    return render(request, 'sources_etav_plot.html', context)

SourceEtaVPlotUpdate(request, pk)

The view to perform the update on the eta-V plot page.

Parameters:

Name Type Description Default
request Request

Django REST Framework request object.

required
pk int

Source object primary key. Defaults to None.

required

Raises:

Type Description
Http404

if a Source with the given pk cannot be found.

Returns:

Type Description
Response

The response for the update page.

Source code in vast_pipeline/views.py
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
@login_required
def SourceEtaVPlotUpdate(request: Request, pk: int) -> Response:
    """The view to perform the update on the eta-V plot page.

    Args:
        request (Request): Django REST Framework request object.
        pk (int, optional): Source object primary key. Defaults to None.

    Raises:
        Http404: if a Source with the given `pk` cannot be found.

    Returns:
        The response for the update page.
    """
    try:
        source = Source.objects.values().get(pk=pk)
    except Source.DoesNotExist:
        raise Http404

    source['wavg_ra_hms'] = deg2hms(source['wavg_ra'], hms_format=True)
    source['wavg_dec_dms'] = deg2dms(source['wavg_dec'], dms_format=True)
    source['wavg_l'], source['wavg_b'] = equ2gal(source['wavg_ra'], source['wavg_dec'])

    context = {
        'source': source,
        'sourcefav': (
            SourceFav.objects.filter(
                user__id=request.user.id,
                source__id=source['id']
            )
            .exists()
        ),
        'datatables': []
    }

    return render(request, 'sources_etav_plot_update.html', context)