diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py index 9865af56c9026af554507487e191574be95470f8..09538bb3aa46cba4b4d26155d29c96d8252aa9b1 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/lofar_viewset.py @@ -24,20 +24,32 @@ from django_filters.rest_framework import DjangoFilterBackend, FilterSet, CharFi from django_filters import filterset from rest_framework.filters import OrderingFilter from django.contrib.postgres.fields import JSONField, ArrayField +from django.db.models.fields import CharField, TextField from copy import deepcopy -class LOFARDefaultFilterSet(FilterSet): - FILTER_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS) - FILTER_DEFAULTS.update({ +FILTER_OVERRIDES = { JSONField: { - 'filter_class': CharFilter + 'filter_class': CharFilter, + 'extra': lambda f: {'lookup_expr': 'icontains'} }, ArrayField: { 'filter_class': CharFilter, 'extra': lambda f: {'lookup_expr': 'icontains'} }, - }) + CharField: { + 'filter_class': CharFilter, + 'extra': lambda f: {'lookup_expr': 'icontains'} + }, + TextField: { + 'filter_class': CharFilter, + 'extra': lambda f: {'lookup_expr': 'icontains'} + } + } + +class LOFARDefaultFilterSet(FilterSet): + FILTER_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS) + FILTER_DEFAULTS.update(FILTER_OVERRIDES) class LOFARFilterBackend(DjangoFilterBackend): diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index e26967e7ec46fbaa1a6a49a8bde1ac1d5a9bb3a1..7bfb0f1a186c72099d037c16b8268377ebe254a3 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -8,6 +8,7 @@ from django.http import JsonResponse from django.contrib.auth import get_user_model User = get_user_model() from django_filters import rest_framework as filters +from django_filters import fields as filter_fields import django_property_filter as property_filters from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework import status @@ -22,7 +23,7 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg.openapi import Parameter from drf_yasg import openapi -from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOFARNestedViewSet, AbstractTemplateViewSet, LOFARCopyViewSet, LOFARFilterBackend +from lofar.sas.tmss.tmss.tmssapp.viewsets.lofar_viewset import LOFARViewSet, LOFARNestedViewSet, AbstractTemplateViewSet, LOFARCopyViewSet, LOFARFilterBackend, FILTER_OVERRIDES from lofar.sas.tmss.tmss.tmssapp import models from lofar.sas.tmss.tmss.tmssapp import serializers from lofar.sas.tmss.tmss.tmssapp.adapters.reports import create_cycle_report, create_project_report @@ -43,7 +44,7 @@ import logging import dateutil from django.core.exceptions import ObjectDoesNotExist - +from django import forms logger = logging.getLogger(__name__) @@ -511,21 +512,63 @@ class ProjectCategoryViewSet(LOFARViewSet): serializer_class = serializers.ProjectCategorySerializer +class NumberInFilter(filters.BaseInFilter, filters.NumberFilter): + """ + Custom filter for comma-separated lists of numbers + """ + pass + + +class FloatRangeField(filter_fields.RangeField): + """ + Custom field for use in FloatRangeFilter + """ + def __init__(self, fields=None, *args, **kwargs): + if fields is None: + fields = ( + forms.FloatField(), + forms.FloatField()) + super().__init__(fields, *args, **kwargs) + + +class FloatRangeFilter(filters.RangeFilter): + """ + Custom RangeFilter that works on float fields in a JSONField, which by default is not happy with numeric filtering + of django_filter due to internal use of a Decimal object. + """ + field_class = FloatRangeField + + class SchedulingUnitDraftPropertyFilter(property_filters.PropertyFilterSet): - project = property_filters.PropertyCharFilter(field_name='project') + project = property_filters.PropertyCharFilter(field_name='project', lookup_expr='icontains') + scheduling_set = property_filters.PropertyCharFilter(field_name='scheduling_set', lookup_expr='icontains') # remove to filter based on list of options + priority_rank = filters.RangeFilter(field_name='priority_rank') + id = NumberInFilter(field_name='id', lookup_expr='in') + id_min = filters.NumberFilter(field_name='id', lookup_expr='gte') + id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') + scheduling_unit_blueprints = filters.ModelMultipleChoiceFilter(field_name='scheduling_unit_blueprints', queryset=models.SchedulingUnitBlueprint.objects.all()) + duration_min = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='gte') + duration_max = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='lte') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + + # Todo: unfortunately we have to reference by task name for the lookup, which is not constant between tasks. + # If there is no fixed name for the task you want to display here, we have to look this up somewhere else (tasks?) + # or we could potentially determine the wanted angles in some function that iterates tasks and checks for sth like + # task['specifications_template'] == 'target observation' and then we just expose the pointing as a property. + # If exact filtering is sufficient, we could also consider to simply filter on a string 'angle1: <user input>' in + # requirements_doc__tasks. + angle1 = FloatRangeFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__angle1') + angle2 = FloatRangeFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__angle2') + direction_type = filters.CharFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__direction_type', lookup_expr='iexact') + + observation_strategy_template_name = filters.CharFilter(field_name='observation_strategy_template__name', lookup_expr='icontains') + observation_strategy_template_description = filters.CharFilter(field_name='observation_strategy_template__description', lookup_expr='icontains') class Meta: model = models.SchedulingUnitDraft fields = '__all__' - filter_overrides = { - models.JSONField: { - 'filter_class': property_filters.CharFilter, - }, - models.ArrayField: { - 'filter_class': property_filters.CharFilter, - 'extra': lambda f: {'lookup_expr': 'icontains'} - }, - } + filter_overrides = FILTER_OVERRIDES class SchedulingUnitDraftViewSet(LOFARViewSet): @@ -872,24 +915,43 @@ class TaskBlueprintCopyToTaskDraftViewSet(LOFARCopyViewSet): return Response(content, status=status.HTTP_404_NOT_FOUND) +class ModelChoiceInFilter(filters.BaseInFilter, filters.ModelChoiceFilter): + """ + Custom filter for comma-separated lists of references + """ + pass + class SchedulingUnitBlueprintPropertyFilter(property_filters.PropertyFilterSet): start_time = property_filters.PropertyIsoDateTimeFromToRangeFilter(field_name='start_time') stop_time = property_filters.PropertyIsoDateTimeFromToRangeFilter(field_name='stop_time') - project = property_filters.PropertyCharFilter(field_name='project') - status = property_filters.PropertyCharFilter(field_name='status') + project = property_filters.PropertyCharFilter(field_name='project', lookup_expr='icontains') + status = property_filters.PropertyChoiceFilter(field_name='status', choices=tuple((i.value, i.value) for i in models.SchedulingUnitBlueprint.Status), lookup_expr='iexact') + priority_rank = filters.RangeFilter(field_name='priority_rank') + id = NumberInFilter(field_name='id', lookup_expr='in') + id_min = filters.NumberFilter(field_name='id', lookup_expr='gte') + id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') + duration_min = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='gte') + duration_max = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='lte') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + draft = ModelChoiceInFilter(field_name='draft', lookup_expr='in', queryset=models.SchedulingUnitDraft.objects.all()) + draft_min = filters.NumberFilter(field_name='draft__id', lookup_expr='gte') + draft_max = filters.NumberFilter(field_name='draft__id', lookup_expr='lte') + + # Todo: see SchedulingUnitDraftPropertyFilter + angle1 = FloatRangeFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__angle1') + angle2 = FloatRangeFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__angle2') + direction_type = filters.CharFilter(field_name='requirements_doc__tasks__Target Observation__specifications_doc__SAPs__0__digital_pointing__direction_type', lookup_expr='iexact') + + scheduling_set = property_filters.PropertyCharFilter(field_name='draft__scheduling_set', lookup_expr='icontains') # remove to filter based on list of options + observation_strategy_template = filters.ModelChoiceFilter(field_name='draft__observation_strategy_template', queryset=models.SchedulingUnitObservingStrategyTemplate.objects.all()) + observation_strategy_template_name = filters.CharFilter(field_name='draft__observation_strategy_template__name', lookup_expr='icontains') + observation_strategy_template_description = filters.CharFilter(field_name='draft__observation_strategy_template__description', lookup_expr='icontains') class Meta: model = models.SchedulingUnitBlueprint fields = '__all__' - filter_overrides = { - models.JSONField: { - 'filter_class': property_filters.CharFilter, - }, - models.ArrayField: { - 'filter_class': property_filters.CharFilter, - 'extra': lambda f: {'lookup_expr': 'icontains'} - }, - } + filter_overrides = FILTER_OVERRIDES class SchedulingUnitBlueprintViewSet(LOFARViewSet): @@ -1020,10 +1082,34 @@ class SchedulingUnitBlueprintNestedViewSet(LOFARNestedViewSet): else: return models.SchedulingUnitBlueprint.objects.all() +class TaskDraftPropertyFilter(property_filters.PropertyFilterSet): + id = NumberInFilter(field_name='id', lookup_expr='in') + id_min = filters.NumberFilter(field_name='id', lookup_expr='gte') + id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') + scheduling_unit_draft = ModelChoiceInFilter(field_name='scheduling_unit_draft', lookup_expr='in', queryset=models.SchedulingUnitDraft.objects.all()) + scheduling_unit_draft_min = filters.NumberFilter(field_name='scheduling_unit_draft__id', lookup_expr='gte') + scheduling_unit_draft_max = filters.NumberFilter(field_name='scheduling_unit_draft__id', lookup_expr='lte') + scheduling_unit_draft_name = filters.CharFilter(field_name='scheduling_unit_draft__name', lookup_expr='icontains') + task_blueprints = filters.ModelMultipleChoiceFilter(field_name='task_blueprints', queryset=models.TaskBlueprint.objects.all()) + duration_min = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='gte') + duration_max = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='lte') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + relative_start_time_min = property_filters.PropertyDurationFilter(field_name='relative_start_time', lookup_expr='gte') + relative_start_time_max = property_filters.PropertyDurationFilter(field_name='relative_start_time', lookup_expr='lte') + relative_stop_time_min = property_filters.PropertyDurationFilter(field_name='relative_stop_time', lookup_expr='gte') + relative_stop_time_max = property_filters.PropertyDurationFilter(field_name='relative_stop_time', lookup_expr='lte') + + class Meta: + model = models.TaskDraft + fields = '__all__' + filter_overrides = FILTER_OVERRIDES + class TaskDraftViewSet(LOFARViewSet): queryset = models.TaskDraft.objects.all() serializer_class = serializers.TaskDraftSerializer + filter_class = TaskDraftPropertyFilter # prefetch all reverse related references from other models on their related_name to avoid a ton of duplicate queries queryset = queryset.prefetch_related('first_scheduling_relation') \ @@ -1147,9 +1233,42 @@ class TaskDraftNestedViewSet(LOFARNestedViewSet): return models.TaskDraft.objects.all() +class TaskBlueprintPropertyFilter(property_filters.PropertyFilterSet): + id = NumberInFilter(field_name='id', lookup_expr='in') + id_min = filters.NumberFilter(field_name='id', lookup_expr='gte') + id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') + scheduling_unit_blueprint = ModelChoiceInFilter(field_name='scheduling_unit_blueprint', lookup_expr='in', queryset=models.SchedulingUnitBlueprint.objects.all()) + scheduling_unit_blueprint_min = filters.NumberFilter(field_name='scheduling_unit_blueprint__id', lookup_expr='gte') + scheduling_unit_blueprint_max = filters.NumberFilter(field_name='scheduling_unit_blueprint__id', lookup_expr='lte') + scheduling_unit_blueprint_name = filters.CharFilter(field_name='scheduling_unit_blueprint__name', lookup_expr='icontains') + draft = ModelChoiceInFilter(field_name='draft', lookup_expr='in', queryset=models.TaskDraft.objects.all()) + draft_min = filters.NumberFilter(field_name='draft__id', lookup_expr='gte') + draft_max = filters.NumberFilter(field_name='draft__id', lookup_expr='lte') + duration_min = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='gte') + duration_max = property_filters.PropertyDurationFilter(field_name='duration', lookup_expr='lte') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + relative_start_time_min = property_filters.PropertyDurationFilter(field_name='relative_start_time', lookup_expr='gte') + relative_start_time_max = property_filters.PropertyDurationFilter(field_name='relative_start_time', lookup_expr='lte') + relative_stop_time_min = property_filters.PropertyDurationFilter(field_name='relative_stop_time', lookup_expr='gte') + relative_stop_time_max = property_filters.PropertyDurationFilter(field_name='relative_stop_time', lookup_expr='lte') + start_time = property_filters.PropertyIsoDateTimeFromToRangeFilter(field_name='start_time') + stop_time = property_filters.PropertyIsoDateTimeFromToRangeFilter(field_name='stop_time') + status = property_filters.PropertyChoiceFilter(field_name='status', choices=tuple((i.value, i.value) for i in models.SubtaskState.Choices), lookup_expr='iexact') + subtasks = filters.ModelMultipleChoiceFilter(field_name='subtasks', queryset=models.Subtask.objects.all()) + subtasks_min = filters.NumberFilter(field_name='subtasks__id', lookup_expr='gte') + subtasks_max = filters.NumberFilter(field_name='subtasks__id', lookup_expr='lte') + + class Meta: + model = models.TaskBlueprint + fields = '__all__' + filter_overrides = FILTER_OVERRIDES + + class TaskBlueprintViewSet(LOFARViewSet): queryset = models.TaskBlueprint.objects.all() serializer_class = serializers.TaskBlueprintSerializer + filter_class = TaskBlueprintPropertyFilter # prefetch all reverse related references from other models on their related_name to avoid a ton of duplicate queries queryset = queryset.prefetch_related('first_scheduling_relation')\ diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py index 9f5eb818ed3c56674f8b4ac87b77479ee24e175e..05a4a49fd9ec04920bc124f172e8c2f110994a23 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py @@ -28,7 +28,7 @@ # We should probably also fully test behavior wrt mandatory and nullable fields. from dateutil import parser -from datetime import datetime +from datetime import datetime, timedelta import unittest import logging logger = logging.getLogger('lofar.'+__name__) @@ -54,6 +54,8 @@ test_data_creator = TMSSRESTTestDataCreator(BASE_URL, AUTH) tmss_test_env.populate_schemas() +from lofar.common.json_utils import add_defaults_to_json_object_for_schema + # todo: for overall speed improvements, but also for clarity, it would be nice to switch to django.test.TestCase # in order to separate the db content between them. Investigated why that currently yields a ton of 404 errors. # Note that mixing unittest.TestCase and django.test.TestCase does not seem to isolate tests properly. @@ -1517,8 +1519,8 @@ class SchedulingUnitDraftTestCase(unittest.TestCase): Test we can filter on this property, which is explicitly named on the model-specific property filter """ # setup - project_1 = models.Project.objects.create(**Project_test_data(name='myproject1')) - project_2 = models.Project.objects.create(**Project_test_data(name='myproject2')) + project_1 = models.Project.objects.create(**Project_test_data(name='myproject_draft_partial_1_%s' % uuid.uuid4())) + project_2 = models.Project.objects.create(**Project_test_data(name='myproject_draft_partial_2_%s' % uuid.uuid4())) scheduling_set_1 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project_1)) scheduling_set_2 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project_2)) su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4(), scheduling_set=scheduling_set_1)) @@ -1528,14 +1530,153 @@ class SchedulingUnitDraftTestCase(unittest.TestCase): response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?project=%s' % project_1.name, 200) response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?project=%s' % project_2.name, 200) response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?project=foo', 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?project=myproject_draft_partia', 200) self.assertEqual(response_1['count'], 1) self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) self.assertEqual(response_2['count'], 1) self.assertEqual(response_2['results'][0]['name'], su_draft_2.name) self.assertEqual(response_3['count'], 0) + self.assertEqual(response_4['count'], 2) + self.assertEqual({response_4['results'][0]['name'], response_4['results'][1]['name']}, + {su_draft_1.name, su_draft_2.name}) + + def test_GET_SchedulingUnitDraft_view_filters_for_id(self): + """ + Test we can filter on this field, which is explicitly named on the model-specific property filter + """ + # setup + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4())) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4())) + su_draft_3 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud3_%s' % uuid.uuid4())) + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?id=%s' % su_draft_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?id=%s,%s' % (su_draft_1.id, su_draft_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?id=%s' % 999999, 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?id_min=%s&id_max=%s' % (su_draft_2.id, su_draft_3.id), 200) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['count'], 2) + self.assertEqual(response_2['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['results'][1]['name'], su_draft_3.name) + self.assertEqual(response_3['count'], 0) + self.assertEqual(response_4['count'], 2) + self.assertEqual(response_4['results'][0]['name'], su_draft_2.name) + self.assertEqual(response_4['results'][1]['name'], su_draft_3.name) + + def test_GET_SchedulingUnitDraft_view_filters_for_blueprints(self): + """ + Test we can filter on this related field, which is explicitly named on the model-specific property filter + """ + # setup + models.SchedulingUnitBlueprint.objects.all().delete() + models.SchedulingUnitDraft.objects.all().delete() + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4())) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4())) + su_draft_3 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud3_%s' % uuid.uuid4())) + su_blueprint_1 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub1_%s' % uuid.uuid4(), draft=su_draft_1)) + su_blueprint_2 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub2_%s' % uuid.uuid4(), draft=su_draft_2)) + su_blueprint_3 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub3_%s' % uuid.uuid4(), draft=su_draft_3)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?scheduling_unit_blueprints=%s' % su_blueprint_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?scheduling_unit_blueprints=%s&scheduling_unit_blueprints=%s' % (su_blueprint_1.id, su_blueprint_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?scheduling_unit_blueprints=%s' % 999999, 400) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['count'], 2) + self.assertEqual(response_2['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['results'][1]['name'], su_draft_3.name) + self.assertIn('Select a valid choice', str(response_3)) + + def test_GET_SchedulingUnitDraft_view_filters_for_priority_rank(self): + """ + Test we can filter on this property, which is explicitly named on the model-specific property filter + """ + # setup + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4()), priority_rank=0.111) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4()), priority_rank=0.222) + su_draft_3 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud3_%s' % uuid.uuid4()), priority_rank=0.333) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?priority_rank_min=%s&priority_rank_max=%s' % (0.110, 0.112), 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?priority_rank_min=%s&priority_rank_max=%s' % (0.2, 0.4), 200) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['count'], 2) + self.assertEqual(response_2['results'][0]['name'], su_draft_2.name) + self.assertEqual(response_2['results'][1]['name'], su_draft_3.name) + + def test_GET_SchedulingUnitDraft_view_filters_for_duration(self): + """ + Test we can filter on this property, which is explicitly named on the model-specific property filter + """ + # setup + models.SchedulingUnitBlueprint.objects.all().delete() + models.SchedulingUnitDraft.objects.all().delete() + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4())) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4())) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?duration_min=%s&duration_max=%s' % ('PT1H', 'PT2H'), 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?duration_min=%s&duration_max=%s' % ('PT0S', 'PT1H'), 200) + + self.assertEqual(response_1['count'], 0) + self.assertEqual(response_2['count'], 2) + self.assertEqual({result['name'] for result in response_2['results']}, + {su_draft_1.name, su_draft_2.name}) + + def test_GET_SchedulingUnitDraft_view_filters_for_pointing(self): + """ + Test we can filter on this property, which is explicitly named on the model-specific property filter + """ + # setup + strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 CTC+pipelines") + specs_1 = add_defaults_to_json_object_for_schema(strategy_template.template, + strategy_template.scheduling_unit_template.schema) + specs_2 = add_defaults_to_json_object_for_schema(strategy_template.template, + strategy_template.scheduling_unit_template.schema) + specs_1['tasks']['Target Observation']['specifications_doc']['SAPs'][0]['digital_pointing'] = {'angle1': 0.333, 'angle2': 0.999, 'direction_type': 'J2000'} + specs_2['tasks']['Target Observation']['specifications_doc']['SAPs'][0]['digital_pointing'] = {'angle1': 0.111, 'angle2': 0.777, 'direction_type': 'J2000'} + + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4(), template=strategy_template.scheduling_unit_template, observation_strategy_template=strategy_template, requirements_doc=specs_1)) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4(), template=strategy_template.scheduling_unit_template, observation_strategy_template=strategy_template, requirements_doc=specs_2)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?angle1_min=%s' % 0.222, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?angle2_max=%s' % 0.888, 200) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['count'], 1) + self.assertEqual(response_2['results'][0]['name'], su_draft_2.name) + + def test_GET_SchedulingUnitBlueprint_view_filters_for_strategy_template(self): + """ + Test we can filter on this field, which is explicitly named on the model-specific property filter + """ + # setup + template_1 = models.SchedulingUnitObservingStrategyTemplate.objects.create(**SchedulingUnitObservingStrategyTemplate_test_data(name='suost1_%s' % uuid.uuid4())) + template_2 = models.SchedulingUnitObservingStrategyTemplate.objects.create(**SchedulingUnitObservingStrategyTemplate_test_data(name='suost1_%s' % uuid.uuid4())) + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4(), observation_strategy_template=template_1)) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4(), observation_strategy_template=template_2)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?observation_strategy_template=%s' % template_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?observation_strategy_template_name=%s' % template_2.name[:-3], 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?observation_strategy_template_name=%s' % 'gibberish', 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?observation_strategy_template=%s' % 999999, 400) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_draft_1.name) + self.assertEqual(response_2['count'], 1) + self.assertEqual(response_2['results'][0]['name'], su_draft_2.name) + self.assertEqual(response_3['count'], 0) class TaskDraftTestCase(unittest.TestCase): @@ -1735,6 +1876,71 @@ class TaskDraftTestCase(unittest.TestCase): GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?copy_reason=gibberish', 400) + def test_TaskDraft_filters_for_TaskBlueprints(self): + + # setup + task_draft_1 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_draft_2 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_draft_3 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_blueprint_1 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data()) + task_blueprint_1.draft = task_draft_1 + task_blueprint_1.save() + task_blueprint_2 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data()) + task_blueprint_2.draft = task_draft_2 + task_blueprint_2.save() + task_blueprint_3 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data()) + task_blueprint_3.draft = task_draft_3 + task_blueprint_3.save() + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?task_blueprints=%s' % task_blueprint_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?task_blueprints=%s' % task_blueprint_2.id, 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?task_blueprints=%s&task_blueprints=%s' % (task_blueprint_1.id, task_blueprint_3.id), 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?task_blueprints=gibberish', 400) + self.assertIn(task_draft_1.name, str(response_1)) + self.assertNotIn(task_draft_2.name, str(response_1)) + self.assertNotIn(task_draft_3.name, str(response_1)) + self.assertNotIn(task_draft_1.name, str(response_2)) + self.assertIn(task_draft_2.name, str(response_2)) + self.assertNotIn(task_draft_3.name, str(response_2)) + self.assertIn(task_draft_1.name, str(response_3)) + self.assertNotIn(task_draft_2.name, str(response_3)) + self.assertIn(task_draft_3.name, str(response_3)) + + def test_TaskDraft_filters_for_SchedulingUnitDraft(self): + + # setup + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data("sud_%s" % uuid.uuid4())) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data("sud_%s" % uuid.uuid4())) + su_draft_3 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data("sud_%s" % uuid.uuid4())) + task_draft_1 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4(), scheduling_unit_draft=su_draft_1)) + task_draft_2 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4(), scheduling_unit_draft=su_draft_2)) + task_draft_3 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4(), scheduling_unit_draft=su_draft_3)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?scheduling_unit_draft=%s' % su_draft_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?scheduling_unit_draft=%s,%s' % (su_draft_2.id, su_draft_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?scheduling_unit_draft_min=%s&scheduling_unit_draft_max=%s' % (su_draft_3.id, su_draft_3.id), 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?scheduling_unit_draft_name=%s' % su_draft_2.name[:-3], 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_draft/?scheduling_unit_draft=%s' % 9999999, 400) + self.assertEqual(response_1['count'], 1) + self.assertIn(task_draft_1.name, str(response_1)) + self.assertNotIn(task_draft_2.name, str(response_1)) + self.assertNotIn(task_draft_3.name, str(response_1)) + self.assertEqual(response_2['count'], 2) + self.assertNotIn(task_draft_1.name, str(response_2)) + self.assertIn(task_draft_2.name, str(response_2)) + self.assertIn(task_draft_3.name, str(response_2)) + self.assertEqual(response_3['count'], 1) + self.assertNotIn(task_draft_1.name, str(response_3)) + self.assertNotIn(task_draft_2.name, str(response_3)) + self.assertIn(task_draft_3.name, str(response_3)) + self.assertEqual(response_4['count'], 1) + self.assertNotIn(task_draft_1.name, str(response_4)) + self.assertIn(task_draft_2.name, str(response_4)) + self.assertNotIn(task_draft_3.name, str(response_4)) + + class TaskRelationDraftTestCase(unittest.TestCase): @classmethod def setUpClass(cls) -> None: @@ -2063,8 +2269,8 @@ class SchedulingUnitBlueprintTestCase(unittest.TestCase): Test we can filter on this property, which is explicitly named on the model-specific property filter """ # setup - project_1 = models.Project.objects.create(**Project_test_data(name='myproject1_%s' % uuid.uuid4())) - project_2 = models.Project.objects.create(**Project_test_data(name='myproject2_%s' % uuid.uuid4())) + project_1 = models.Project.objects.create(**Project_test_data(name='myproject_partial_1_%s' % uuid.uuid4())) + project_2 = models.Project.objects.create(**Project_test_data(name='myproject_partial_2_%s' % uuid.uuid4())) scheduling_set_1 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project_1)) scheduling_set_2 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project_2)) su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(scheduling_set=scheduling_set_1)) @@ -2076,12 +2282,16 @@ class SchedulingUnitBlueprintTestCase(unittest.TestCase): response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?project=%s' % project_1.name, 200) response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?project=%s' % project_2.name, 200) response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?project=foo', 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?project=myproject_partia', 200) self.assertEqual(response_1['count'], 1) self.assertEqual(response_1['results'][0]['name'], su_blueprint_1.name) self.assertEqual(response_2['count'], 1) self.assertEqual(response_2['results'][0]['name'], su_blueprint_2.name) self.assertEqual(response_3['count'], 0) + self.assertEqual(response_4['count'], 2) + self.assertEqual(response_4['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_4['results'][1]['name'], su_blueprint_2.name) def test_GET_SchedulingUnitBlueprint_view_filters_for_output_pinned(self): """ @@ -2103,6 +2313,91 @@ class SchedulingUnitBlueprintTestCase(unittest.TestCase): self.assertEqual(response_false['count'], 1) self.assertEqual(response_false['results'][0]['name'], su_blueprint_false.name) + def test_GET_SchedulingUnitBlueprint_view_filters_for_draft(self): + """ + Test we can filter on this property, which is explicitly named on the model-specific property filter + """ + # setup + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4())) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4())) + su_draft_3 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud3_%s' % uuid.uuid4())) + su_blueprint_1 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub1_%s' % uuid.uuid4())) + su_blueprint_2 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub2_%s' % uuid.uuid4())) + su_blueprint_3 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub3_%s' % uuid.uuid4())) + su_blueprint_1.draft = su_draft_1 + su_blueprint_1.save() + su_blueprint_2.draft = su_draft_2 + su_blueprint_2.save() + su_blueprint_3.draft = su_draft_3 + su_blueprint_3.save() + + # assertresponse_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_draft/?scheduling_unit_blueprints=%s' % 999999, 200) + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?draft=%s' % su_draft_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?draft=%s,%s' % (su_draft_1.id, su_draft_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?draft=%s' % 999999, 400) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?draft_min=%s&draft_max=%s' % (su_draft_2.id, su_draft_3.id), 200) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_2['count'], 2) + self.assertEqual(response_2['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_2['results'][1]['name'], su_blueprint_3.name) + self.assertIn('Select a valid choice', str(response_3)) + self.assertEqual(response_4['count'], 2) + self.assertEqual(response_4['results'][0]['name'], su_blueprint_2.name) + self.assertEqual(response_4['results'][1]['name'], su_blueprint_3.name) + + + def test_GET_SchedulingUnitBlueprint_view_filters_for_id(self): + """ + Test we can filter on this field, which is explicitly named on the model-specific property filter + """ + # setup + su_blueprint_1 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub1_%s' % uuid.uuid4())) + su_blueprint_2 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub2_%s' % uuid.uuid4())) + su_blueprint_3 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub3_%s' % uuid.uuid4())) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?id=%s' % su_blueprint_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?id=%s,%s' % (su_blueprint_1.id, su_blueprint_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?id=%s' % 999999, 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?id_min=%s&id_max=%s' % (su_blueprint_2.id, su_blueprint_3.id), 200) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_2['count'], 2) + self.assertEqual(response_2['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_2['results'][1]['name'], su_blueprint_3.name) + self.assertEqual(response_3['count'], 0) + self.assertEqual(response_4['count'], 2) + self.assertEqual(response_4['results'][0]['name'], su_blueprint_2.name) + self.assertEqual(response_4['results'][1]['name'], su_blueprint_3.name) + + + def test_GET_SchedulingUnitBlueprint_view_filters_for_strategy_template(self): + """ + Test we can filter on this field, which is explicitly named on the model-specific property filter + """ + # setup + template_1 = models.SchedulingUnitObservingStrategyTemplate.objects.create(**SchedulingUnitObservingStrategyTemplate_test_data(name='suost1_%s' % uuid.uuid4())) + template_2 = models.SchedulingUnitObservingStrategyTemplate.objects.create(**SchedulingUnitObservingStrategyTemplate_test_data(name='suost1_%s' % uuid.uuid4())) + su_draft_1 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud1_%s' % uuid.uuid4(), observation_strategy_template=template_1)) + su_draft_2 = models.SchedulingUnitDraft.objects.create(**SchedulingUnitDraft_test_data(name='sud2_%s' % uuid.uuid4(), observation_strategy_template=template_2)) + su_blueprint_1 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub1_%s' % uuid.uuid4(), draft=su_draft_1)) + su_blueprint_2 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data(name='sub2_%s' % uuid.uuid4(), draft=su_draft_2)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?observation_strategy_template=%s' % template_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?observation_strategy_template_name=%s' % template_2.name[:-3], 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?observation_strategy_template_name=%s' % 'gibberish', 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/scheduling_unit_blueprint/?observation_strategy_template=%s' % 999999, 400) + + self.assertEqual(response_1['count'], 1) + self.assertEqual(response_1['results'][0]['name'], su_blueprint_1.name) + self.assertEqual(response_2['count'], 1) + self.assertEqual(response_2['results'][0]['name'], su_blueprint_2.name) + self.assertEqual(response_3['count'], 0) + class TaskBlueprintTestCase(unittest.TestCase): @classmethod @@ -2305,25 +2600,116 @@ class TaskBlueprintTestCase(unittest.TestCase): response_data = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/%s/' % task_blueprint.id, 200) assertUrlList(self, response_data['subtasks'], [subtask_1, subtask_2]) - @unittest.skip("This test is dependend on previous tests (in the results list). ToDo: fix it.") def test_TaskBlueprint_contains_lists_of_related_TaskRelationBlueprint(self): # setup - test_data_1 = TaskBlueprint_test_data() - test_data_2 = TaskBlueprint_test_data() - trbt_test_data_1 = TaskRelationBlueprint_test_data() - trbt_test_data_2 = TaskRelationBlueprint_test_data() - task_blueprint = models.TaskBlueprint.objects.create(**test_data_1) - task_relation_blueprint_1 = models.TaskRelationBlueprint.objects.create(**trbt_test_data_1) - task_relation_blueprint_1.producer = task_blueprint - task_relation_blueprint_1.save() - task_relation_blueprint_2 = models.TaskRelationBlueprint.objects.create(**trbt_test_data_2) - task_relation_blueprint_2.consumer = task_blueprint - task_relation_blueprint_2.save() + task_blueprint = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data()) + task_relation_blueprint_1 = models.TaskRelationBlueprint.objects.create(**TaskRelationBlueprint_test_data(producer=task_blueprint)) + task_relation_blueprint_2 = models.TaskRelationBlueprint.objects.create(**TaskRelationBlueprint_test_data(consumer=task_blueprint)) + # assert response_data = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/%s/' % task_blueprint.id, 200) - assertUrlList(self, response_data['produced_by'], [task_relation_blueprint_1]) - assertUrlList(self, response_data['consumed_by'], [task_relation_blueprint_2]) + assertUrlList(self, response_data['consumed_by'], [task_relation_blueprint_1]) + assertUrlList(self, response_data['produced_by'], [task_relation_blueprint_2]) + + def test_TaskBlueprint_filters_for_SchedulingUnitBlueprint(self): + + # setup + su_blueprint_1 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data("sub_%s" % uuid.uuid4())) + su_blueprint_2 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data("sub_%s" % uuid.uuid4())) + su_blueprint_3 = models.SchedulingUnitBlueprint.objects.create(**SchedulingUnitBlueprint_test_data("sub_%s" % uuid.uuid4())) + task_blueprint_1 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data("tb_%s" % uuid.uuid4(), scheduling_unit_blueprint=su_blueprint_1)) + task_blueprint_2 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data("tb_%s" % uuid.uuid4(), scheduling_unit_blueprint=su_blueprint_2)) + task_blueprint_3 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data("tb_%s" % uuid.uuid4(), scheduling_unit_blueprint=su_blueprint_3)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?scheduling_unit_blueprint=%s' % su_blueprint_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?scheduling_unit_blueprint=%s,%s' % (su_blueprint_2.id, su_blueprint_3.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?scheduling_unit_blueprint_min=%s&scheduling_unit_blueprint_max=%s' % (su_blueprint_3.id, su_blueprint_3.id), 200) + response_4 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?scheduling_unit_blueprint_name=%s' % su_blueprint_2.name[:-3], 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?scheduling_unit_blueprint=%s' % 99999, 400) + self.assertEqual(response_1['count'], 1) + self.assertIn(task_blueprint_1.name, str(response_1)) + self.assertEqual(response_2['count'], 2) + self.assertNotIn(task_blueprint_1.name, str(response_2)) + self.assertIn(task_blueprint_2.name, str(response_2)) + self.assertIn(task_blueprint_3.name, str(response_2)) + self.assertEqual(response_3['count'], 1) + self.assertNotIn(task_blueprint_1.name, str(response_3)) + self.assertNotIn(task_blueprint_2.name, str(response_3)) + self.assertIn(task_blueprint_3.name, str(response_3)) + self.assertEqual(response_4['count'], 1) + self.assertNotIn(task_blueprint_1.name, str(response_4)) + self.assertIn(task_blueprint_2.name, str(response_4)) + self.assertNotIn(task_blueprint_3.name, str(response_4)) + + def test_TaskBlueprint_filters_for_TaskDraft(self): + + # setup + task_draft_1 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_draft_2 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_draft_3 = models.TaskDraft.objects.create(**TaskDraft_test_data("td_%s" % uuid.uuid4())) + task_blueprint_1 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4(), task_draft=task_draft_1)) + task_blueprint_2 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4(), task_draft=task_draft_2)) + task_blueprint_3 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4(), task_draft=task_draft_3)) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?draft=%s' % task_draft_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?draft_min=%s&draft_max=%s' % (task_draft_2.id, task_draft_2.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?draft=%s,%s' % (task_draft_1.id, task_draft_3.id), 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?draft=%s' % 9999999, 400) + self.assertIn(task_blueprint_1.name, str(response_1)) + self.assertNotIn(task_blueprint_2.name, str(response_1)) + self.assertNotIn(task_blueprint_3.name, str(response_1)) + self.assertNotIn(task_blueprint_1.name, str(response_2)) + self.assertIn(task_blueprint_2.name, str(response_2)) + self.assertNotIn(task_blueprint_3.name, str(response_2)) + self.assertIn(task_blueprint_1.name, str(response_3)) + self.assertNotIn(task_blueprint_2.name, str(response_3)) + self.assertIn(task_blueprint_3.name, str(response_3)) + + def test_TaskBlueprint_filters_for_Subtask(self): + + # setup + task_blueprint_1 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4())) + task_blueprint_2 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4())) + task_blueprint_3 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4())) + subtask_1 = models.Subtask.objects.create(**Subtask_test_data()) + subtask_2 = models.Subtask.objects.create(**Subtask_test_data()) + subtask_3 = models.Subtask.objects.create(**Subtask_test_data()) + subtask_1.task_blueprints.set([task_blueprint_1]) + subtask_2.task_blueprints.set([task_blueprint_2]) + subtask_3.task_blueprints.set([task_blueprint_3]) + subtask_1.save() + subtask_2.save() + subtask_3.save() + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?subtasks=%s' % subtask_1.id, 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?subtasks_min=%s&subtasks_max=%s' % (subtask_2.id, subtask_2.id), 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?subtasks=%s&subtasks=%s' % (subtask_1.id, subtask_3.id), 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?subtasks=%s' % 9999999, 400) + self.assertIn(task_blueprint_1.name, str(response_1)) + self.assertNotIn(task_blueprint_2.name, str(response_1)) + self.assertNotIn(task_blueprint_3.name, str(response_1)) + self.assertNotIn(task_blueprint_1.name, str(response_2)) + self.assertIn(task_blueprint_2.name, str(response_2)) + self.assertNotIn(task_blueprint_3.name, str(response_2)) + self.assertIn(task_blueprint_1.name, str(response_3)) + self.assertNotIn(task_blueprint_2.name, str(response_3)) + self.assertIn(task_blueprint_3.name, str(response_3)) + + def test_TaskBlueprint_filters_for_status(self): + + # setup + task_blueprint_1 = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(name="tb_%s" % uuid.uuid4())) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=defined', 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=obsolete', 200) + GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=gibberish', 400) + self.assertGreater(response_1['count'], 0) + self.assertEqual(response_2['count'], 0) class TaskRelationBlueprintTestCase(unittest.TestCase): diff --git a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js index dc2f2b7dbd0495ec23b3a30705f487c381a391af..9a016cc545728c252f21233473f88ce90897b55d 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/components/ViewTable.js @@ -5,7 +5,7 @@ import _, { filter } from 'lodash'; import moment from 'moment'; import { useHistory } from "react-router-dom"; import { OverlayPanel } from 'primereact/overlaypanel'; -//import {InputSwitch} from 'primereact/inputswitch'; +import { InputMask } from 'primereact/inputmask'; import { InputText } from 'primereact/inputtext'; import { Calendar } from 'primereact/calendar'; import { Paginator } from 'primereact/paginator'; @@ -18,12 +18,18 @@ import { MultiSelect } from 'primereact/multiselect'; import { RadioButton } from 'primereact/radiobutton'; import { useExportData } from "react-table-plugins"; import { ProgressBar } from 'primereact/progressbar'; -//import UIConstants from '../utils/ui.constants'; + +import "flatpickr/dist/flatpickr.css"; +import Flatpickr from "react-flatpickr"; +import confirmDatePlugin from "flatpickr/dist/plugins/confirmDate/confirmDate"; +import shortcutButtonsPlugin from "shortcut-buttons-flatpickr"; + import UtilService from '../../src/services/util.service' import Papa from "papaparse"; import JsPDF from "jspdf"; import "jspdf-autotable"; import TableUtil from "../utils/table.util"; +import Validator from "../utils/validator" let doServersideFilter = false; let tbldata = [], filteredData = []; @@ -41,6 +47,7 @@ let showCSV = false; let multiSelectOption = {}; let filterCallback = null; let tableOptionsState = null; +let tableToolTipsState = {}; let setLoaderFunction = null; let showFilterOption = null; let hasFilters = false; @@ -49,7 +56,7 @@ let tmpTableData = null; let currentTableName = null; let storeFilter = false; let storage_array = []; - +//let confirmDatePlugin = new confirmDatePlugin(); // Define a default UI for filtering function GlobalFilter({ preGlobalFilteredRows, @@ -109,7 +116,8 @@ function DefaultColumnFilter({ return ( <> <div className="table-filter" onClick={e => { e.stopPropagation() }} style={{marginRight: '5px'}}> - <input title="Enter the exact value to be searched and press 'Enter' key" + <input + title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter few characters and press ‘Enter’ key to search"} value={value} //***TO REMOVE - INCOMING CHANGE WAS value={filterValue || ''} onChange={e => { setValue(e.target.value); @@ -250,9 +258,9 @@ function SelectColumnFilter({ _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); if (isCleared) { hasFilters = false; - if (filtered) { + //if (filtered) { filterCallback(tableOptionsState, setLoaderFunction); - } + //} } else { tableOptionsState.filters.push({id: Header, value: event.target.value}); filterCallback(tableOptionsState, setLoaderFunction); @@ -263,6 +271,7 @@ function SelectColumnFilter({ return ( <div onClick={e => { e.stopPropagation() }}> <select + title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select a value from list to search"} className= {columnclassname[0][Header]} style={{ height: '24.2014px', @@ -392,6 +401,7 @@ function MultiSelectColumnFilter({ maxSelectedLabels="1" selectedItemsLabel="{0} Selected" className="multi-select" + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select one or more value from list to search"} /> </div> </div> @@ -466,6 +476,7 @@ function MultiSelectFilter({ maxSelectedLabels="1" selectedItemsLabel="{0} Selected" className="multi-select" + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select one or more value from list to search"} /> </div> </div> @@ -509,7 +520,9 @@ function SliderColumnFilter({ if (storeFilter) { TableUtil.saveFilter(currentTableName, Header, e.value); } - }} /> + }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select range value to search"} + /> </div> ) } @@ -557,17 +570,19 @@ function BooleanColumnFilter({ return ( <div onClick={e => { e.stopPropagation() }}> <TriStateCheckbox value={value} style={{ 'width': '15px', 'height': '24.2014px' }} on - onChange={(e) => { - setValue(e.target.value); - setFilter(e.target.value || undefined); - setFiltered(true); - if (storeFilter) { - TableUtil.saveFilter(currentTableName, Header, e.target.value); - } - if (doServersideFilter) { - callServerFilter(e.value); - } - }} /> + onChange={(e) => { + setValue(e.target.value); + setFilter(e.target.value || undefined); + setFiltered(true); + if (storeFilter) { + TableUtil.saveFilter(currentTableName, Header, e.target.value); + } + if (doServersideFilter) { + callServerFilter(e.value); + } + }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Select checkbox (True/False/None) to search"} + /> </div> ) } @@ -684,7 +699,7 @@ function DateRangeColumnFilter({ } }, [filterValue, value]); // Function to call the server side filtering - const callServerFilter = (event, isCleared) => { + const callServerFilter = (value, isCleared) => { hasFilters = true; if (isCleared) { hasFilters = false; @@ -693,7 +708,13 @@ function DateRangeColumnFilter({ filterCallback(tableOptionsState, setLoaderFunction); } } else { + let filterColumn = _.find(tableOptionsState.filters, {id: Header }); + if(filterColumn) { + filterColumn.value = value; filterCallback(tableOptionsState, setLoaderFunction); + } else { + filterCallback(tableOptionsState, setLoaderFunction); + } } }; return ( @@ -703,27 +724,128 @@ function DateRangeColumnFilter({ onChange={(e) => { setValue(e.value); setFilter(e.value); - if (value !== 'Invalid date' && doServersideFilter) { - setFiltered(true); - callServerFilter(e); - } if(storeFilter) { TableUtil.saveFilter(currentTableName, Header, e.target.value); } + if ((value !== '' && value !== 'Invalid date' ) && doServersideFilter) { + setFiltered(true); + callServerFilter(e.target.value); + } }} showIcon></Calendar> - {value && <i onClick={(e) => { setFilter(undefined); setValue(''); setFiltered(false); + {value && <i onClick={(e) => { + setFilter(undefined); setValue([]); setFiltered(false); if(storeFilter){ TableUtil.saveFilter(currentTableName, Header, [] ); } if (doServersideFilter) { setFilter(undefined); setValue(''); - callServerFilter(e, true); + callServerFilter(e.target.value, true); } }} className="tb-cal-reset fa fa-times" />} </div> ) } + +// This is a custom filter UI that uses a +// flatpickr range calendar to set the value +function FlatpickrRangeColumnFilter({ + column: { setFilter, filterValue, Header }, +}) { + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(null); + } + if (storeFilter) { + const filter = TableUtil.getFilter(currentTableName, Header); + if (filter === '') { + TableUtil.saveFilter(currentTableName, Header, [] ); + setFilter(undefined); + } + const filterValue = _.map(filter, date => {return new Date(date)}); + if (filterValue[1] && !value ){ + setValue(filterValue); + setFilter(filterValue); + } + } + }, [filterValue, value]); + // Function to call the server side filtering + const callServerFilter = (value, isCleared) => { + hasFilters = true; + if (isCleared) { + hasFilters = false; + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + filterCallback(tableOptionsState, setLoaderFunction); + } else { + let filterColumn = _.find(tableOptionsState.filters, {id: Header }); + if(filterColumn) { + filterColumn.value = value; + filterCallback(tableOptionsState, setLoaderFunction); + } else if (!filterColumn && value && value.length === 2) { + // Here the above condition placed because the Start/End time filters is not consistency in tableOptionsState.filters + filterColumn = {id: Header, value: value} + tableOptionsState.filters.push(filterColumn); + filterCallback(tableOptionsState, setLoaderFunction); + } else { + filterCallback(tableOptionsState, setLoaderFunction); + } + } + }; + return ( + <div className="table-filter" onClick={e => { e.stopPropagation() }}> + <Flatpickr data-enable-time data-input + options={{ "inlineHideInput": true, + "wrap": true, + "enableSeconds": true, + "time_24hr": true, + "minuteIncrement": 1, + "allowInput": true, + "mode": "range", + "defaultHour": 0, + "plugins": [new confirmDatePlugin()] + }} + title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter the date & time range to search and press ‘Ok’ button"} + value={filterValue} + onClose={value => { + if(value.length === 2) { + setValue(value); + setFilter(value); + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, value); + } + if ((value !== '' && value !== 'Invalid date' ) && doServersideFilter) { + setFiltered(true); + callServerFilter(value); + } + } + }} + > + <input type="text" data-input className={`p-inputtext p-component calendar-input`} /> + <button class="p-button p-component p-button-icon-only calendar-button" data-toggle + title="Click to select the date range" > + <i class="fas fa-calendar"></i> + </button> + <button class="p-button p-component p-button-icon-only calendar-reset" + onClick={(value) => { + setFilter(undefined); setValue([]); setFiltered(false);filterValue = []; + if(storeFilter){ + TableUtil.saveFilter(currentTableName, Header, [] ); + } + if (doServersideFilter) { + setFilter(undefined); + setValue(''); + callServerFilter(value, true); + } + }} title="Clear date range" > + <i class="fa fa-times" style={{color:'white', marginTop:'-2.85px'}} ></i> + </button> + </Flatpickr> + </div> + ) +} + // This is a custom filter UI that uses a // calendar to set the value function CalendarColumnFilter({ @@ -1049,11 +1171,358 @@ function RangeColumnFilter({ ); } +/** + * Number range rilter + * @param {number} param0 : Range value for min and max filters + * @returns + */ +function NumberRangeFilter({ + column: { filterValue = [], preFilteredRows, setFilter, id, Header }, +}) { + let [rangeValue, setRangeValue] = useState([0,0]); + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(''); + } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if (filterValue) { + setFiltered(true); + } + if(!value){ + setValue(filterValue); + //setFilter(filterValue); + } + } + }, [filterValue, value]); + + // Function to call the server side filtering + const callServerFilter = (event, isCleared) => { + hasFilters = true; + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + if (isCleared) { + hasFilters = false; + if (filtered) { + filterCallback(tableOptionsState, setLoaderFunction); + } + } else { + tableOptionsState.filters.push({id: Header, value: rangeValue}); + filterCallback(tableOptionsState, setLoaderFunction); + } + }; + + return ( + <div + style={{ + alignItems: 'center' + }} + > + <InputText + value={value[0]} + type="number" + tooltip="Enter the minimum value to search" + onKeyUp={(e) => { + if (e.key === "Enter" && doServersideFilter) { + TableUtil.saveFilter(currentTableName, Header, rangeValue); + setFiltered(true); + callServerFilter(e, false); + } + }} + onChange={e => { + setFilter(undefined); setFiltered(false); + const val = e.target.value; + let max = rangeValue[1]; + setValue([val,max]); + setFilter([val,max] || undefined); + setRangeValue([val,max]); + filterValue[0] = val; + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, [val,max]); + setFilter([val,max]); + } + }} + style={{ + width: '65px', + height: '25px' + // marginRight: '0.5rem', + }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Minimum Range value and press ‘Enter’ key to search"} + /> + + <InputText + value={value[1]} + type="number" + tooltip="Enter the maximum value to search" + onKeyUp={(e) => { + if (e.key === "Enter" && doServersideFilter) { + setFiltered(true); + callServerFilter(e, false); + } + }} + onChange={e => { + const val = e.target.value; + let min = rangeValue[0]; + setRangeValue([min,val]); + filterValue[1] = val; + setValue([min,val]); + setFilter([min,val] || undefined); + if(storeFilter) { + TableUtil.saveFilter(currentTableName, Header, [min,val]); + setFilter([min,val]); + } + }} + + style={{ + width: '65px', + height: '25px' + }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Maximum Range value and press ‘Enter’ key to search"} + /> + </div> + ) +} + +// Priority Rank Range Filter +function RankRangeFilter({ + column: { filterValue = [], preFilteredRows, setFilter, id, Header }, +}) { + let [rangeValue, setRangeValue] = useState([0,0]); + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(''); + //setRangeValue([]) + } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if (filterValue) { + setFiltered(true); + } + if(!value){ + setValue(filterValue); + //setFilter(filterValue); + } + } + }, [filterValue, value]); + + // Function to call the server side filtering + const callServerFilter = (event, isCleared) => { + hasFilters = true; + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + if (isCleared) { + hasFilters = false; + if (filtered) { + filterCallback(tableOptionsState, setLoaderFunction); + } + } else { + tableOptionsState.filters.push({id: Header, value: rangeValue}); + filterCallback(tableOptionsState, setLoaderFunction); + } + }; + + return ( + <div + style={{ + alignItems: 'center' + }} + > + <input type="decimal" + title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Minimum Range value and press ‘Enter’ key to search"} + max="1" + min="0" + className="p-inputtext p-component" + value={value[0]} + step="0.0001" + onKeyUp={(e) => { + if (e.key === "Enter" && doServersideFilter) { + TableUtil.saveFilter(currentTableName, Header, rangeValue); + setFiltered(true); + callServerFilter(e, false); + } + }} + onChange={e => { + setFilter(undefined); setFiltered(false); + let val = e.target.value; + val = val.replace(/([^0-9.]+)/, ""); + const match = /(\d{0,1})[^.]*((?:\.\d{0,4})?)/g.exec(val); + val = match[1] + match[2]; + if (val == '' || (val >= 0 && val <= 1)) { + let max = rangeValue[1]; + setValue([val,max]); + setFilter([val,max] || undefined); + setRangeValue([val,max]); + filterValue[0] = val; + if(storeFilter) { + //TableUtil.saveFilter(currentTableName, Header, [val,max]); + setFilter([val,max]); + } + } else { + e.target.value = rangeValue[0]; + } + }} + style={{ + width: '75px', + height: '25px' + }} + /> + + <input type="decimal" + title={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Maximum Range value and press ‘Enter’ key to search"} + max="1" + min="0" + className="p-inputtext p-component" + value={value[1]} + step="0.0001" + onKeyUp={(e) => { + if (e.key === "Enter" && doServersideFilter) { + TableUtil.saveFilter(currentTableName, Header, rangeValue); + setFiltered(true); + callServerFilter(e, false); + } + }} + onChange={e => { + setFilter(undefined); setFiltered(false); + let val = e.target.value; + val = val.replace(/([^0-9.]+)/, ""); + const match = /(\d{0,1})[^.]*((?:\.\d{0,4})?)/g.exec(val); + val = match[1] + match[2]; + if (val == '' || (val >= 0 && val <= 1)) { + let min = rangeValue[0]; + setRangeValue([min,val]); + filterValue[1] = val; + setValue([min,val]); + setFilter([min,val] || undefined); + if(storeFilter) { + //TableUtil.saveFilter(currentTableName, Header, [min,val]); + setFilter([min,val]); + } + } else { + e.target.value = rangeValue[0]; + } + }} + style={{ + width: '75px', + height: '25px' + }} + /> + </div> + ) +} + +// Duration Range Filter +function DurationRangeFilter({ + column: { filterValue = [], preFilteredRows, setFilter, id, Header }, +}) { + let [rangeValue, setRangeValue] = useState([0,0]); + const [value, setValue] = useState(''); + const [filtered, setFiltered] = useState(false); + React.useEffect(() => { + if (!filterValue && value) { + setValue(''); + } + if (storeFilter) { + const filterValue = TableUtil.getFilter(currentTableName, Header); + if (filterValue) { + setFiltered(true); + } + if(!value){ + setValue(filterValue); + //setFilter(filterValue); + } + } + }, [filterValue, value]); + + // Function to call the server side filtering + const callServerFilter = (event, isCleared) => { + hasFilters = true; + _.remove(tableOptionsState.filters, function(filter) { return filter.id === Header }); + if (isCleared) { + hasFilters = false; + if (filtered) { + filterCallback(tableOptionsState, setLoaderFunction); + } + } else { + tableOptionsState.filters.push({id: Header, value: rangeValue}); + filterCallback(tableOptionsState, setLoaderFunction); + } + }; + + return ( + <div + onKeyPress={(e) => { + if (e.key === "Enter" && doServersideFilter) { + TableUtil.saveFilter(currentTableName, Header, rangeValue); + setFiltered(true); + callServerFilter(e, false); + } + }} + style={{ + alignItems: 'center' + }} + > + <InputMask mask="99:99:99" + value={value[0]} + placeholder="HH:mm:ss" + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Minimum Range value in HH:mm:ss format and press ‘Enter’ key to search"} + onChange={e => { + setFilter(undefined); setFiltered(false); + let val = e.target.value; + if (val.includes(":") && !Validator.isValidHHmmss(val, true)) { + val = rangeValue[0]; + } + let max = rangeValue[1]; + setValue([val,max]); + setFilter([val,max] || undefined); + setRangeValue([val,max]); + filterValue[0] = val; + if(storeFilter) { + //TableUtil.saveFilter(currentTableName, Header, [val,max]); + setFilter([val,max]); + } + }} + style={{ + width: '85px', + height: '25px' + }} + /> + + <InputMask mask="99:99:99" + value={value[1]} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Maximum Range value in HH:mm:ss format and press ‘Enter’ key to search"} + placeholder="HH:mm:ss" + onChange={e => { + setFilter(undefined); setFiltered(false); + let val = e.target.value; + if (val.includes(":") && !Validator.isValidHHmmss(val, true)) { + val = rangeValue[1]; + } + let min = rangeValue[0]; + setValue([min,val]); + setFilter([min,val] || undefined); + setRangeValue([min,val]); + filterValue[1] = val; + if(storeFilter) { + //TableUtil.saveFilter(currentTableName, Header, [min,val]); + setFilter([min,val]); + } + }} + style={{ + width: '85px', + height: '25px' + }} + /> + </div> + ) +} + // This is a custom UI for our 'between' or number range // filter. It uses two number boxes and filters rows to // ones that have values between the two function NumberRangeColumnFilter({ - column: { filterValue = [], preFilteredRows, setFilter, id }, + column: { filterValue = [], preFilteredRows, setFilter, id, Header }, }) { const [errorProps, setErrorProps] = useState({}); const [maxErr, setMaxErr] = useState(false); @@ -1088,6 +1557,7 @@ function NumberRangeColumnFilter({ height: '25px' // marginRight: '0.5rem', }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Minimum Range value and press ‘Enter’ key to search"} /> <InputText value={filterValue[1] || ''} @@ -1115,6 +1585,7 @@ function NumberRangeColumnFilter({ height: '25px' // marginLeft: '0.5rem', }} + tooltip={(tableToolTipsState[Header])?tableToolTipsState[Header]:"Enter Maximum Range value and press ‘Enter’ key to search"} /> </div> ) @@ -1154,6 +1625,11 @@ const filterTypes = { fn: DateRangeColumnFilter, type: dateRangeFilterFn }, + 'flatpickrDateRange': { + fn: FlatpickrRangeColumnFilter, + type: dateRangeFilterFn + }, + 'fromdatetime': { fn: DateTimeColumnFilter, type: fromDatetimeFilterFn @@ -1169,6 +1645,18 @@ const filterTypes = { 'minMax': { fn: NumberRangeColumnFilter, type: 'between' + }, + 'numberRangeMinMax': { + fn: NumberRangeFilter, + type: 'between' + }, + 'rankMinMax': { + fn: RankRangeFilter, + type: 'between' + }, + 'durationMinMax': { + fn: DurationRangeFilter, + type: 'between' } }; // Let the table remove the filter if the string is empty @@ -1684,7 +2172,20 @@ function ViewTable(props) { let columns = []; let defaultdataheader = Object.keys(defaultheader[0]); let optionaldataheader = Object.keys(optionalheader[0]); - + + // Get Tooltips for each column if provided and used in filter components + tableToolTipsState = {}; + for(const headerId of defaultdataheader) { + if (defaultheader[0][headerId].tooltip) { + tableToolTipsState[defaultheader[0][headerId].name] = defaultheader[0][headerId].tooltip; + } + } + for(const headerId of optionaldataheader) { + if (optionalheader[0][headerId].tooltip) { + tableToolTipsState[optionalheader[0][headerId].name] = optionalheader[0][headerId].tooltip; + } + } + /* If allowRowSelection property is true for the component, add checkbox column as 1st column. If the record has property to select, enable the checkbox */ if (allowRowSelection) { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js index b5bc772bd08007df994fb7efac1b5cbbfd433be5..7065001a263f0d3df35bba651f63f7ffa2c380d4 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/SchedulingUnitList.js @@ -33,31 +33,29 @@ class SchedulingUnitList extends Component{ super(props); this. setToggleBySorting(); this.defaultcolumns = { - status: { - name: "Status", - filter: "select", - }, + status: {name: "Status",filter: "select"}, workflowStatus: {name: "Workflow Status"}, suid: {name: "Scheduling Unit ID"}, project:{name:"Project",}, name:{name:"Name",}, description: {name: "Description"}, priority: {name: "Priority"}, - suSet: {name: "Scheduling Set"}, - template_name: {name: "Template Name"}, - template_description: {name: "Template Description"}, + scheduling_set: {name: "Scheduling Set",}, + observation_strategy_template_name: {name: "Template Name"}, + observation_strategy_template_description: {name: "Template Description"}, start_time:{ name:"Start Time", - filter:"fromdatetime", + filter:"dateRange", format:UIConstants.CALENDAR_DATETIME_FORMAT }, stop_time:{ name:"End Time", - filter:"todatetime", + filter:"dateRange", format:UIConstants.CALENDAR_DATETIME_FORMAT }, duration:{ name:"Duration (HH:mm:ss)", + filter: "numberRangeMinMax", format:UIConstants.CALENDAR_TIME_FORMAT }, station_group: {name: "Stations (CS/RS/IS)"}, @@ -76,6 +74,7 @@ class SchedulingUnitList extends Component{ this.mainStationGroups = {}; this.suTypeList = [{name: 'Blueprint'}, {name: 'Draft'}] this.totalPage = 0; + this.statusList = []; this.state = { showSpinner: false, suType: this.suUIAttr['listType'] || "Draft", @@ -120,34 +119,34 @@ class SchedulingUnitList extends Component{ optionalcolumns: [{ actionpath:"actionpath", draft:{name: "Linked Blueprint Id"}, - priority_queue: {name: "Priority Queue"}, - priority_rank: {name: "Priority Rank"}, + priority_queue: {name: "Priority Queue", filter: "select"}, + priority_rank: {name: "Priority Rank",filter: "numberRangeMinMax"}, observation_strategy_template_id:{ name: "Template ID", filter: "select" }, created_at:{ name:"Created_At", - filter: "fromdatetime", + filter: "dateRange", format:UIConstants.CALENDAR_DATETIME_FORMAT }, updated_at:{ name:"Updated_At", - filter: "todatetime", + filter: "dateRange", format:UIConstants.CALENDAR_DATETIME_FORMAT }, output_pinned:{name: "Prevent Autodeletion"}, }], columnclassname: [{ - "Scheduling Unit ID":"filter-input-50", + "Scheduling Unit ID":"filter-input-100", "Template":"filter-input-50", "Template Description": "filter-input-200", "Scheduling Set": "filter-input-150", "Project":"filter-input-100", "Priority Rank":"filter-input-50", "Duration (HH:mm:ss)":"filter-input-75", - "Linked Draft ID":"filter-input-50", - "Linked Blueprint ID":"filter-input-50", + "Linked Draft ID":"filter-input-100", + "Linked Blueprint ID":"filter-input-100", "Type": "filter-input-75", "Status":"filter-input-100", "workflowStatus":"filter-input-100", @@ -326,6 +325,12 @@ class SchedulingUnitList extends Component{ } columnDef[key]['filter'] = UIConstants.FILTER_MAP[suFilters.data.filters[tempKey].type]; } + } else if (key === 'draft' && suFilters.data.filters['scheduling_unit_blueprints']) { + columnDef[key]['disableSortBy'] = !_.includes(suFilters.data.ordering, 'scheduling_unit_blueprints'); + columnDef[key]['disableFilters'] = false; + } else if (key === 'duration' && suFilters.data.filters['duration_min']) { + columnDef[key]['disableSortBy'] = !_.includes(suFilters.data.ordering, 'duration_min'); + columnDef[key]['disableFilters'] = false; } else if (columnDef[key]['name']) { columnDef[key]['disableSortBy'] = true; columnDef[key]['disableFilters'] = true; @@ -387,12 +392,25 @@ class SchedulingUnitList extends Component{ this.removeColumns(tmpOptionalColumns, columnDefinitionToBeRemove); } if(suFilters) { + this.getStatusList(suFilters); tmpDefaulColumns = this.getAPIFilter(suFilters, tmpDefaulColumns); tmpOptionalColumns = this.getAPIFilter(suFilters, tmpOptionalColumns); await this.setState({tmpDefaulcolumns: [tmpDefaulColumns], tmpOptionalcolumns:[tmpOptionalColumns], tmpColumnOrders: tmpColumnOrders, columnMap: this.columnMap}) } } + /** + * Get Status list frol filter + * @param {Array} suFilters + */ + getStatusList(suFilters) { + if (suFilters.data.filters['status']) { + suFilters.data.filters['status'].choices.forEach(choice => { + this.statusList.push(choice.value); + }) + } + } + /** * * @param {Boolean} isInitial - true => While loading page, false => while doing server side access @@ -442,8 +460,8 @@ class SchedulingUnitList extends Component{ if (!this.props.project || (this.props.project && this.props.project === scheduleunit.scheduling_set.project.name)) { scheduleunit['status'] = null; scheduleunit['workflowStatus'] = null; - scheduleunit['template_name'] = scheduleunit.observation_strategy_template.name; - scheduleunit['template_description'] = scheduleunit.observation_strategy_template.description; + scheduleunit['observation_strategy_template_name'] = scheduleunit.observation_strategy_template.name; + scheduleunit['observation_strategy_template_description'] = scheduleunit.observation_strategy_template.description; scheduleunit['draft'] = this.getLinksList(scheduleunit.scheduling_unit_blueprints_ids, 'blueprint'); scheduleunit['task_content'] = this.getTaskTypeGroupCounts(scheduleunit['task_drafts']); scheduleunit['station_group'] = this.getStationGroup(scheduleunit).counts; @@ -452,13 +470,13 @@ class SchedulingUnitList extends Component{ scheduleunit['duration'] = moment.utc((scheduleunit.duration || 0)*1000).format('HH:mm:ss'); scheduleunit.project = scheduleunit.scheduling_set.project.name; scheduleunit.canSelect = true; - scheduleunit.suSet = scheduleunit.scheduling_set.name; scheduleunit.links = ['Project', 'id']; scheduleunit.linksURL = { 'Project': `/project/view/${scheduleunit.scheduling_set.project.name}`, 'id': `/schedulingunit/view/draft/${scheduleunit.id}` }; scheduleunit['suid'] = scheduleunit.id; + scheduleunit.scheduling_set = scheduleunit.scheduling_set.name; output.push(scheduleunit); } } @@ -503,13 +521,13 @@ class SchedulingUnitList extends Component{ } else { for( const scheduleunit of scheduleunits){ this.draftIds.push(scheduleunit.draft_id); - scheduleunit['suSet'] = scheduleunit.draft.scheduling_set.name; scheduleunit['project'] = scheduleunit.draft.scheduling_set.project.name; scheduleunit['links'] = ['Project', 'id']; scheduleunit['linksURL'] = { 'Project': `/project/view/${scheduleunit.draft.scheduling_set.project.name}`, 'id': `/schedulingunit/view/blueprint/${scheduleunit.id}` }; + scheduleunit['scheduling_set'] = scheduleunit.draft.scheduling_set.name; scheduleunit['workflowStatus'] = null; const workflowProcess = _.find(this.state.workflowProcesses, ['su', scheduleunit.id]); scheduleunit['workflowStatus'] = workflowProcess?workflowProcess.status: null; @@ -517,8 +535,8 @@ class SchedulingUnitList extends Component{ scheduleunit.type= 'Blueprint'; scheduleunit['actionpath'] ='/schedulingunit/view/blueprint/'+scheduleunit.id; scheduleunit['task_content'] = this.getTaskTypeGroupCounts(scheduleunit['task_blueprints']); - scheduleunit['template_name'] = scheduleunit.draft? scheduleunit.draft.observation_strategy_template.name : ''; - scheduleunit['template_description'] = scheduleunit.draft? scheduleunit.draft.observation_strategy_template.description : ''; + scheduleunit['observation_strategy_template_name'] = scheduleunit.draft? scheduleunit.draft.observation_strategy_template.name : ''; + scheduleunit['observation_strategy_template_description'] = scheduleunit.draft? scheduleunit.draft.observation_strategy_template.description : ''; scheduleunit['observation_strategy_template_id'] = scheduleunit.draft? scheduleunit.draft.observation_strategy_template.id : ''; scheduleunit['station_group'] = this.getStationGroup(scheduleunit).counts; scheduleunit['draft'] = this.getLinksList([scheduleunit.draft_id], 'draft'); //key 'drafts' used in filters, so use after process or change variable while fetching @@ -1122,12 +1140,57 @@ class SchedulingUnitList extends Component{ if(filters.length > 0 ) { for( const filter of filters) { if (filter.id === 'Start Time') { - this.filterQry += 'start_time_after='+filter.value+'&start_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&'; + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += 'start_time_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += 'start_time_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } } else if (filter.id === 'End Time') { - //let end = moment.utc(moment(filter.value, 'YYYY-MM-DDTHH:mm:SS').format("YYYY-MM-DDTHH:mm:SS")); - //end = moment(end, "DD-MM-YYYY").add(1, 'days'); - //filter.value = _.replace(filter.value, '00:00:00', '23:59:59'); - this.filterQry += 'stop_time_after='+filter.value+'&stop_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&' + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += 'stop_time_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += 'stop_time_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } + } else if (filter.id === 'Linked Blueprint ID' && filter.value != '') { + const values = _.split(filter.value, ","); + for ( const value of values) { + this.filterQry += 'scheduling_unit_blueprints='+value+"&"; + } + } else if (filter.id === 'Priority Rank' && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = _.split(filter.value, ","); + if (values[0] && values[0] !== '' && values[0]>0) { + this.filterQry += columnDetails.orgField+"_min" +'='+parseFloat(values[0])+'&'; + } + if (values[1] && values[1] !== '' && values[1]>0) { + this.filterQry += columnDetails.orgField+"_max" +'='+parseFloat(values[1])+'&'; + } + } else if ((filter.id === 'Created_At' || filter.id === 'Updated_At') && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += columnDetails.orgField+'_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += columnDetails.orgField+'_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } + } else if (filter.id === 'Duration (HH:mm:ss)' && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = _.split(filter.value, ","); + if (values[0] && !isNaN(values[0]) && values[0]>0) { + this.filterQry += columnDetails.orgField+"_min" +'=PT'+parseFloat(values[0])+'S&'; + } + if (values[1] && !isNaN(values[1]) && values[1]>0) { + this.filterQry += columnDetails.orgField+"_max" +'=PT'+parseFloat(values[1])+'S&'; + } + } else { let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); if(columnDetails) { @@ -1180,7 +1243,7 @@ class SchedulingUnitList extends Component{ getFilterOptions(id) { let options = null; if(id && id === 'Status') { - options = UIConstants.SU_STATUS; + options = this.statusList;//UIConstants.SU_STATUS; } else if (id === 'Linked Draft ID') { options = _.sortBy(this.draftIds); } @@ -1553,4 +1616,4 @@ class SchedulingUnitList extends Component{ } } -export default SchedulingUnitList +export default SchedulingUnitList \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js index fab924a2355fb6d4e865b32471e91f288457d761..521de333fb73efe8270b8aabf42c01e90d8226ea 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/ViewSchedulingUnit.js @@ -158,6 +158,7 @@ class ViewSchedulingUnit extends Component { taskStatus: [], userPermission: {permissions: AuthStore.getState().userRolePermission} } + this.statusList = []; this.access_denied_message = "Don't have permission"; this.actions = []; this.stations = []; @@ -202,6 +203,7 @@ class ViewSchedulingUnit extends Component { this.getCleanUpDialogContent = this.getCleanUpDialogContent.bind(this); this.cleanUpSUTask = this.cleanUpSUTask.bind(this); this.confirmAutoDeletion = this.confirmAutoDeletion.bind(this); + this.getFilterOptions = this.getFilterOptions.bind(this); } componentDidUpdate(prevProps, prevState) { @@ -216,7 +218,33 @@ class ViewSchedulingUnit extends Component { this.setState({ showTaskRelationDialog: !this.state.showTaskRelationDialog }); } + /** + * Get Status list for UI drop downl + */ + async getStatusList() { + const taskFilters = await TaskService.getTaskFilterDefinition('blueprint'); + if (taskFilters && taskFilters.data.filters['status']) { + taskFilters.data.filters['status'].choices.forEach(choice => { + this.statusList.push(choice.value); + }) + } + } + + /** + * Get Option-list values for Select Dropdown filter in 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getFilterOptions(id) { + let options = null; + if(id && id === 'Status') { + options = this.statusList; + } + return options; + } + async componentDidMount() { + this.getStatusList(); let permission = (await AuthUtil.getUserRolePermission()); permission = { permissions: permission.userRolePermission}; this.pageUpdated = true; @@ -1409,6 +1437,7 @@ class ViewSchedulingUnit extends Component { optionalcolumns={this.state.optionalcolumns} columnclassname={this.state.columnclassname} columnOrders={this.state.columnOrders} + showFilterOption={this.getFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter showaction="true" keyaccessor="id" paths={this.state.paths} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js index f3eecaff04e84bfdb2311e872a5d7d338e37d602..a9309b9ff0800515c754971097430957f3309734 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/list.js @@ -28,6 +28,8 @@ export class TaskList extends Component { TASK_END_STATUSES = ['finished', 'error', 'cancelled']; TASK_DRAFT_EXPAND = 'task_blueprints,task_blueprints.subtasks,scheduling_unit_draft'; TASK_BLUEPRINT_EXPAND = 'subtasks,subtasks.output_dataproducts,draft,scheduling_unit_blueprint'; + COMMA_SEPARATE_TOOLTIP ='Enter Id and press ‘Enter’ key to search. For multiple values enter comma separated values. For range, provide input like 1..6'; + constructor(props) { super(props); this.getUIAttr(); @@ -76,18 +78,22 @@ export class TaskList extends Component { filter: "select" }, scheduling_unit_draft: { - name:"Scheduling Unit ID" + name:"Scheduling Unit ID", + tooltip: this.COMMA_SEPARATE_TOOLTIP }, scheduling_unit_blueprint: { - name:"Scheduling Unit ID" + name:"Scheduling Unit ID", + tooltip: this.COMMA_SEPARATE_TOOLTIP }, schedulingUnitName: { name:"Scheduling Unit Name"}, id: { - name:"ID" + name:"ID", + tooltip: this.COMMA_SEPARATE_TOOLTIP }, subTaskID: { - name:"Control ID" + name:"Control ID", + tooltip: this.COMMA_SEPARATE_TOOLTIP }, name: { name:"Name" @@ -97,22 +103,25 @@ export class TaskList extends Component { }, start_time: { name: "Start Time", - filter: "date", + filter: "flatpickrDateRange", format: UIConstants.CALENDAR_DATETIME_FORMAT }, stop_time: { name: "End Time", - filter: "date", + filter: "flatpickrDateRange", format: UIConstants.CALENDAR_DATETIME_FORMAT }, duration: { - name:"Duration (HH:mm:ss)" + name:"Duration (HH:mm:ss)", + filter: "durationMinMax", }, relative_start_time: { - name:"Relative Start Time (HH:mm:ss)" + name:"Relative Start Time (HH:mm:ss)", + filter: "durationMinMax", }, relative_stop_time: { - name:"Relative End Time (HH:mm:ss)" + name:"Relative End Time (HH:mm:ss)", + filter: "durationMinMax", }, noOfOutputProducts: { name:"#Dataproducts" @@ -136,19 +145,20 @@ export class TaskList extends Component { name:"Tags" }, draft: { - name:"Linked Draft ID" + name:"Linked Draft ID", + tooltip: this.COMMA_SEPARATE_TOOLTIP }, url: { name:"API URL" }, created_at: { name: "Created at", - filter: "date", + filter: "flatpickrDateRange", format: UIConstants.CALENDAR_DATETIME_FORMAT }, updated_at: { name: "Updated at", - filter: "date", + filter: "flatpickrDateRange", format: UIConstants.CALENDAR_DATETIME_FORMAT }, actionpath: "actionpath" @@ -163,7 +173,7 @@ export class TaskList extends Component { "Cancelled": "filter-input-50", "Duration (HH:mm:ss)": "filter-input-75", "Template ID": "filter-input-50", - "Linked Draft ID": "filter-input-50", + "Linked Draft ID": "filter-input-75", "Relative Start Time (HH:mm:ss)": "filter-input-75", "Relative End Time (HH:mm:ss)": "filter-input-75", "Start Time": "filter-input-150", @@ -173,11 +183,14 @@ export class TaskList extends Component { "Data size": "filter-input-50", "Data size on Disk": "filter-input-50", "Subtask Content": "filter-input-75", - "Linked BluePrint ID": "filter-input-50", - "API URL":"filter-input-175" + "Linked BluePrint ID": "filter-input-75", + "API URL":"filter-input-175", + "Created at":"filter-input-150", + "Updated at":"filter-input-150", }], actions: [] }; + this.statusList = []; this.access_denied_message = "Don't have permission"; this.pageUpdated = true; this.taskTypeList = [{name: 'Blueprint'}, {name: 'Draft'}]; @@ -200,6 +213,7 @@ export class TaskList extends Component { this.getTaskCancelStatusContent = this.getTaskCancelStatusContent.bind(this); this.changeTaskType = this.changeTaskType.bind(this); this.fetchTableData = this.fetchTableData.bind(this); + this.getFilterOptions = this.getFilterOptions.bind(this); } subtaskComponent = (task) => { @@ -617,12 +631,25 @@ export class TaskList extends Component { tmpDefaulColumns = _.omit(tmpDefaulColumns,columnDefinitionToRemove); tmpOptionalColumns = _.omit(tmpOptionalColumns,columnDefinitionToRemove); if(taskFilters) { + this.getStatusList(taskFilters); tmpDefaulColumns = this.getAPIFilter(taskFilters, tmpDefaulColumns); tmpOptionalColumns = this.getAPIFilter(taskFilters, tmpOptionalColumns); await this.setState({tmpDefaulcolumns: [tmpDefaulColumns], tmpOptionalcolumns:[tmpOptionalColumns], tmpColumnOrders: tmpColumnOrders, columnMap: this.columnMap}) } } + /** + * Get Status list frol filter + * @param {Array} suFilters + */ + getStatusList(taskFilters) { + if (taskFilters.data.filters['status']) { + taskFilters.data.filters['status'].choices.forEach(choice => { + this.statusList.push(choice.value); + }) + } + } + getAPIFilter(taskFilters, columnDef) { const defaultColKeys = Object.keys(columnDef); defaultColKeys.forEach(key => { @@ -639,11 +666,35 @@ export class TaskList extends Component { columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, tempKey); columnDef[key]['disableFilters'] = false; if(UIConstants.FILTER_MAP[taskFilters.data.filters[tempKey].type]) { - if (tempKey === 'draft') { //this condition because to avoid dropdown filter for 'Linked Draft ID' + /*if (tempKey === 'draft') { //this condition because to avoid dropdown filter for 'Linked Draft ID' taskFilters.data.filters[tempKey].type = 'CharFilter'; - } + }*/ columnDef[key]['filter'] = UIConstants.FILTER_MAP[taskFilters.data.filters[tempKey].type]; } + } else if (key === 'subTaskID' && taskFilters.data.filters['subtasks']) { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'subtasks'); + columnDef[key]['disableFilters'] = false; + } else if (tempKey === 'schedulingUnitName' && + (taskFilters.data.filters['scheduling_unit_draft_name'] || taskFilters.data.filters['scheduling_unit_blueprint_name'])) { + if (this.state.taskType === 'Draft') { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'scheduling_unit_draft_name'); + columnDef[key]['disableFilters'] = false; + } else { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'scheduling_unit_blueprint_name'); + columnDef[key]['disableFilters'] = false; + } + } else if (key === 'duration' && taskFilters.data.filters['duration_min']) { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'duration_min'); + columnDef[key]['disableFilters'] = false; + } else if (key === 'relative_start_time' && taskFilters.data.filters['relative_start_time_min']) { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'relative_start_time_min'); + columnDef[key]['disableFilters'] = false; + } else if (key === 'relative_stop_time' && taskFilters.data.filters['relative_stop_time_min']) { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'relative_stop_time_min'); + columnDef[key]['disableFilters'] = false; + } else if (key === 'draft' && taskFilters.data.filters['task_blueprints']) { + columnDef[key]['disableSortBy'] = !_.includes(taskFilters.data.ordering, 'task_blueprints'); + columnDef[key]['disableFilters'] = false; } else if (columnDef[key]['name']) { columnDef[key]['disableSortBy'] = true; columnDef[key]['disableFilters'] = true; @@ -725,13 +776,70 @@ export class TaskList extends Component { if(filters.length > 0 ) { for( const filter of filters) { if (filter.id === 'Start Time') { - this.filterQry += 'start_time_after='+filter.value+'&start_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&'; + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += 'start_time_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += 'start_time_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } } else if (filter.id === 'End Time') { - this.filterQry += 'stop_time_after='+filter.value+'&stop_time_before='+_.replace(filter.value, '00:00:00', '23:59:59')+'&' - } else { + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += 'stop_time_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += 'stop_time_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } + } else if (filter.id === 'Scheduling Unit Name') { + if (this.state.taskType === 'Draft') { + this.filterQry += 'scheduling_unit_draft_name='+filter.value+'&'; + } else { + this.filterQry += 'scheduling_unit_blueprint_name='+filter.value+'&'; + } + } else if ((filter.id === 'Duration (HH:mm:ss)' || filter.id === 'Relative Start Time (HH:mm:ss)' || filter.id === 'Relative End Time (HH:mm:ss)') && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = _.split(filter.value, ","); + if (values[0].includes(":")) { + this.filterQry += columnDetails.orgField+"_min" +'=PT'+UnitConverter.getHHmmssToSecs(values[0])+'S&'; + } + if (values[1].includes(":")) { + this.filterQry += columnDetails.orgField+"_max" +'=PT'+UnitConverter.getHHmmssToSecs(values[1])+'S&'; + } + } else if (filter.id === 'Linked Blueprint ID' && filter.value != '') { + const values = UnitConverter.getSubbandOutput(filter.value) + _.split(values, ",").forEach(value =>{ + if(value && _.trim(value) !== '') { + this.filterQry += 'task_blueprints='+_.trim(value)+'&'; + } + }); + } else if ((filter.id === 'Created at' || filter.id === 'Updated at') && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = _.split(filter.value, ","); + if (values.length>2){ + continue; + } + if((values[0] && values[0] != '' && values[0] != 'null') && (values[1] && values[1] != '' && values[1] != 'null')) { + this.filterQry += columnDetails.orgField+'_after='+ moment(new Date(values[0])).format("YYYY-MM-DDT00:00:00")+".000Z&"; + this.filterQry += columnDetails.orgField+'_before='+moment(new Date(values[1])).format("YYYY-MM-DDT23:59:59")+".000Z&"; + } + } else if ((filter.id === 'Control ID') && filter.value != '') { + const values = UnitConverter.getSubbandOutput(filter.value); + _.split(values, ",").forEach(value =>{ + if(value && _.trim(value) !== '') { + this.filterQry += 'subtasks='+_.trim(value)+'&'; + } + }); + } else if ((filter.id === 'Scheduling Unit ID' || filter.id === 'Linked Draft ID'|| filter.id === 'ID') && filter.value != '') { + let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); + const values = UnitConverter.getSubbandOutput(filter.value); + this.filterQry += columnDetails.orgField+'='+values.toString()+"&"; + } else { let columnDetails = _.find(this.state.columnMap, {displayName:filter.id}); if(columnDetails) { - this.filterQry += columnDetails.orgField +'='+filter.value+'&' + this.filterQry += columnDetails.orgField +'='+_.trim(filter.value)+'&' } } } @@ -750,6 +858,19 @@ export class TaskList extends Component { return [this.state.tasks, this.totalPage]; } + /** + * Get Option-list values for Select Dropdown filter in 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getFilterOptions(id) { + let options = null; + if(id && id === 'Status') { + options = this.statusList; + } + return options; + } + render() { if (this.state.redirect) { return <Redirect to={{ pathname: this.state.redirect }}></Redirect> @@ -788,6 +909,7 @@ export class TaskList extends Component { columnclassname={this.state.columnclassname} columnOrders={this.state.tmpColumnOrders} defaultSortColumn={this.defaultSortColumn} + showFilterOption={this.getFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter showaction="true" keyaccessor="id" paths={this.state.paths} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js index 81be74c071bcab3fd94a3bde47ef10948ee35229..08f084530c8cdcb7a6281b004da1eeef74aeeec0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/view.js @@ -228,12 +228,12 @@ export class TaskView extends Component { let task = this.state.task; let cancelledTask = await TaskService.cancelTask(task.id); if (!cancelledTask) { - appGrowl.show({ severity: 'error', summary: 'error', detail: 'Error while cancelling Scheduling Unit' }); + appGrowl.show({ severity: 'error', summary: 'error', detail: 'Error while cancelling Task' }); this.setState({ dialogVisible: false }); } else { task.status = cancelledTask.status; let actions = this.state.actions; - appGrowl.show({ severity: 'success', summary: 'Success', detail: 'Scheduling Unit is cancelled successfully' }); + appGrowl.show({ severity: 'success', summary: 'Success', detail: 'Task is cancelled successfully' }); this.setState({ confirmDialogVisible: false, task: task, actions: actions}); } } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js index cebe73f0aa5a71237abdfb3cc7cc2fad0b557cfd..1d6b270679205a88a36a258e980942d7e49b2fbd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/list.tabs.js @@ -28,6 +28,8 @@ class TimelineListTabs extends Component { this.suListFilterCallback = this.suListFilterCallback.bind(this); this.taskListFilterCallback = this.taskListFilterCallback.bind(this); this.getTaskList = this.getTaskList.bind(this); + this.getSUFilterOptions = this.getSUFilterOptions.bind(this); + this.getTaskFilterOptions = this.getTaskFilterOptions.bind(this); } /** @@ -121,6 +123,32 @@ class TimelineListTabs extends Component { return sortData?[{...sortData}]:[this.defaultSortColumns[listName]]; } + /** + * Get Option-list values for Select Dropdown filter in SU 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getSUFilterOptions(id) { + let options = null; + if(id && id === 'Status') { + options = this.props.suStatusList; + } + return options; + } + + /** + * Get Option-list values for Select Dropdown filter in Task 'Viewtable' + * @param {String} id : Column id + * @returns + */ + getTaskFilterOptions(id) { + let options = null; + if(id && id === 'Status') { + options = this.props.taskStatusList; + } + return options; + } + render() { const taskList = this.getTaskList(); return( @@ -137,6 +165,7 @@ class TimelineListTabs extends Component { defaultSortColumn={this.getSortingColumn("SUListSortColumn")} showaction="true" tablename={`timeline_scheduleunit_list`} + showFilterOption={this.getSUFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter showTopTotal={false} showGlobalFilter={true} showColumnFilter={true} @@ -159,6 +188,7 @@ class TimelineListTabs extends Component { showGlobalFilter={true} showColumnFilter={true} tablename={`timeline_task_list`} + showFilterOption={this.getTaskFilterOptions} //Callback function to provide inputs for option-list in Select Dropdown filter showTopTotal={false} filterCallback={this.taskListFilterCallback} lsKeySortColumn={"TaskListSortColumn"} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js index dcb6ff2d3f8ed5d02224005f2c967f762dd75270..1faf0cf5704099a5f8114ad5884b45fd9680a530 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/view.js @@ -88,7 +88,9 @@ export class TimelineView extends Component { selectedTaskTypes: this.timelineUIAttributes["taskTypes"] || ['observation'], isStationTasksVisible: this.timelineUIAttributes.isStationTasksVisible===undefined?true:this.timelineUIAttributes.isStationTasksVisible, showReservation: this.timelineUIAttributes.showReservation || false, // Flag to show reservations in normal timeline view - userrole: AuthStore.getState() + userrole: AuthStore.getState(), + suStatusList: [], + taskStatusList: [] } this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group this.allStationsGroup = []; @@ -124,6 +126,7 @@ export class TimelineView extends Component { } async componentDidMount() { + this.getStatusList(); const permission = await AuthUtil.getUserRolePermission(); const timelinePermission = permission.userRolePermission.timeline; @@ -1075,6 +1078,27 @@ export class TimelineView extends Component { this.setState({showReservation: !this.state.showReservation}); } + /** + * Get Status list for UI drop down list + */ + async getStatusList() { + let suStatusList = []; + let taskStatusList = []; + const suFilters = await ScheduleService.getSchedulingUnitFilterDefinition('blueprint'); + if (suFilters && suFilters.data.filters['status']) { + suFilters.data.filters['status'].choices.forEach(choice => { + suStatusList.push(choice.value); + }); + } + const taskFilters = await TaskService.getTaskFilterDefinition('blueprint'); + if (taskFilters && taskFilters.data.filters['status']) { + taskFilters.data.filters['status'].choices.forEach(choice => { + taskStatusList.push(choice.value); + }); + } + this.setState({suStatusList: suStatusList, taskStatusList: taskStatusList}); + } + render() { if (this.state.redirect) { return <Redirect to={{ pathname: this.state.redirect }}></Redirect> @@ -1117,7 +1141,10 @@ export class TimelineView extends Component { style={isSUListVisible ? { position: "inherit", borderRight: "3px solid #efefef", paddingTop: "10px" } : { display: 'none' }}> <TimelineListTabs suBlueprintList={this.state.suBlueprintList} suListFilterCallback={this.suListFilterCallback} - reservationList={this.getReservationList()}></TimelineListTabs> + reservationList={this.getReservationList()} + suStatusList={this.state.suStatusList} + taskStatusList={this.state.taskStatusList} + ></TimelineListTabs> </div> {/* Timeline Panel */} <div className={isSUListVisible ? ((isSUDetsVisible || isReservDetsVisible) ? "col-lg-5 col-md-5 col-sm-12" : diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js index 53d12803f8d8edc4d73f6a1c99b181b1b1ebc8b5..e07a63a57a083c1fb822e049d0d5d23689b7eee2 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/week.view.js @@ -12,6 +12,7 @@ import PageHeader from '../../layout/components/PageHeader'; import Timeline from '../../components/Timeline'; import ScheduleService from '../../services/schedule.service'; +import TaskService from '../../services/task.service'; import UtilService from '../../services/util.service'; import UnitConverter from '../../utils/unit.converter'; @@ -64,7 +65,9 @@ export class WeekTimelineView extends Component { suTaskList: [], isSummaryLoading: false, stationGroup: [], - reservationEnabled: true + reservationEnabled: true, + suStatusList: [], + taskStatusList: [] } this.STATUS_BEFORE_SCHEDULED = ['defining', 'defined', 'schedulable']; // Statuses before scheduled to get station_group this.reservations = []; @@ -89,6 +92,7 @@ export class WeekTimelineView extends Component { } async componentDidMount() { + this.getStatusList(); const permission = await AuthUtil.getUserRolePermission(); const weekviewPermission = permission.userRolePermission.weekoverview; let menuOptions = [{ label: 'Add Reservation', icon: "fa fa-", disabled: !weekviewPermission.addreservation, command: () => { this.selectOptionMenu('Add Reservation') } }, @@ -801,6 +805,27 @@ export class WeekTimelineView extends Component { this.timeline.updateTimeline(updatedItemGroupData); } + /** + * Get Status list for UI drop down list + */ + async getStatusList() { + let suStatusList = []; + let taskStatusList = []; + const suFilters = await ScheduleService.getSchedulingUnitFilterDefinition('blueprint'); + if (suFilters && suFilters.data.filters['status']) { + suFilters.data.filters['status'].choices.forEach(choice => { + suStatusList.push(choice.value); + }); + } + const taskFilters = await TaskService.getTaskFilterDefinition('blueprint'); + if (taskFilters && taskFilters.data.filters['status']) { + taskFilters.data.filters['status'].choices.forEach(choice => { + taskStatusList.push(choice.value); + }); + } + this.setState({suStatusList: suStatusList, taskStatusList: taskStatusList}); + } + render() { if (this.state.redirect) { return <Redirect to={{ pathname: this.state.redirect }}></Redirect> @@ -845,6 +870,8 @@ export class WeekTimelineView extends Component { <TimelineListTabs suBlueprintList={this.state.suBlueprintList} suListFilterCallback={this.suListFilterCallback} reservationList={this.getReservationList()} + suStatusList={this.state.suStatusList} + taskStatusList={this.state.taskStatusList} ></TimelineListTabs> </div> {/* Timeline Panel */} diff --git a/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js index 13fe8e24136cd91bce2cb186ea8f078d4c40ca5e..65d51fe61b2805b6ef729f418282f41f35309a06 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/shared/timeline.constants.js @@ -82,7 +82,7 @@ const TimelineConstants = { }], TASK_LIST_DEFAULT_COLUMNS: [{ status: { - name: "Status" + name: "Status", filter: "select" }, do_cancel: { name: "Cancelled" diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js index 649d3d069c973b2d3fd8edc3a172ca57e653fa7c..d89aadf99a448db85546f15d0ffec2b6ec8272b0 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/ui.constants.js @@ -29,6 +29,7 @@ const UIConstants = { 'PropertyIsoDateTimeFromToRangeFilter':'', }, SU_STATUS:['cancelled', 'error', 'defining', 'defined', 'schedulable', 'scheduled','started', 'observing', 'observed', 'processing', 'processed', 'ingesting','finished', 'unschedulable'], - CURRENT_WORKFLOW_STAGE:['Waiting To Be Scheduled','Scheduled','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingesting','Unpin Data','Done'] + CURRENT_WORKFLOW_STAGE:['Waiting To Be Scheduled','Scheduled','QA Reporting (TO)', 'QA Reporting (SDCO)', 'PI Verification', 'Decide Acceptance','Ingesting','Unpin Data','Done'], + SU_PRIORITY_QUEUE:['A','B'], } export default UIConstants; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js index 6df626d7a712848176e495a3da8b87f3c810de1d..a5f59b0b9d6fec5aaeb89b36e1addaccb6b27205 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/unit.converter.js @@ -245,7 +245,25 @@ const UnitConverter = { }); } return statusList; - } + }, + /** + * Convert the string input for subband list to Array + * @param {String} prpOutput + */ + getSubbandOutput(prpOutput) { + const subbandArray = prpOutput ? prpOutput.split(",") : []; + let subbandList = []; + for (const subband of subbandArray) { + const subbandRange = subband.split('..'); + if (subbandRange.length > 1) { + subbandList = subbandList.concat(_.range(subbandRange[0], (parseInt(subbandRange[1]) + 1))); + } else if (subbandRange[0] !== ''){ + subbandList = subbandList.concat(parseInt(subbandRange[0])); + } + } + prpOutput = subbandList; + return prpOutput; + } }; export default UnitConverter; \ No newline at end of file diff --git a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js index 050639115a1bea70a502e340482f360f27134b48..4198a7ea71537b9719213bb0e75a5111c1043ea5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/utils/validator.js @@ -46,6 +46,28 @@ const Validator = { } } return isModified; + }, + /** + * Validate time + * @param {string value with HH:mm:ss format} time + * @returns + */ + isValidHHmmss(time, excludeHour) { + let isValid = true; + if (time && time.includes(':')) { + time = time.replaceAll("_", "0"); + if(excludeHour) { + var timeFormat = /^([0-9]|[0-9][0-9]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$/; + isValid = timeFormat.test(time); + } else { + var timeFormat = /^([0-9]|[0-2][0-3]):([0-9]|[0-5][0-9]):([0-9]|[0-5][0-9])$/; + isValid = timeFormat.test(time); + } + + } else { + isValid = false; + } + return isValid; } };