diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 387b80a73987757fb17e5de2af07d783d0cc97b1..5ac21bcca828a7eb3abed22fd8fc343b3033dd9d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -1071,6 +1071,14 @@ class SchedulingUnitCommonPropertiesMixin: except (ValueError, AttributeError): return [] + @cached_property + def main_observation_antenna_set(self) -> str: + '''return specified antenna_set of the main observation task or None if unknown.''' + try: + return self.main_observation_task.antenna_set + except (ValueError, AttributeError): + return None + @cached_property def earliest_possible_cycle_start_time(self) -> datetime.datetime: '''return the earliest possible start time for this unit's project and cycle(s)''' @@ -1618,6 +1626,13 @@ class TaskCommonPropertiesMixin: stations = sorted(list(set(sum([group['stations'] for group in station_groups], [])))) return stations + @cached_property + def antenna_set(self) -> str: + '''return specified for this task if this is an observation, else None''' + if self.is_observation and self.specifications_doc is not None: + return self.specifications_doc.get('station_configuration', {}).get('antenna_set', None) + return None + def _count_station_sets(self, stations: typing.Iterable[str]) -> (int, int, int): '''return a tuple with the number of stations per Core, Remote & International set.''' num_core = 0 diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py index cd615925cf656df83c6aa3dbfd70b62fc153e6d6..9144062c427fe643571a3e7248890216eeee1561 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py @@ -293,6 +293,46 @@ class SchedulingUnitBlueprintSerializer(DynamicRelationalHyperlinkedModelSeriali return scheduling_unit_blueprint +class SchedulingUnitBlueprintSlimSerializer(serializers.ModelSerializer): + '''A small 'slim' serializer for the most relevant properties.''' + scheduler = serializers.CharField(source='scheduling_constraints_doc.scheduler', label='scheduler', read_only=True) + project = serializers.StringRelatedField(source='draft.scheduling_set.project.name', label='project', read_only=True) + antenna_set = serializers.CharField(source='main_observation_antenna_set', label='antenna_set', read_only=True) + stations = serializers.ListField(source='main_observation_specified_stations', label='stations', read_only=True) + used_stations = serializers.ListField(source='main_observation_used_stations', label='used_stations', read_only=True) + duration = FloatDurationField(read_only=True) + on_sky_duration = FloatDurationField(read_only=True) + + class Meta: + model = models.SchedulingUnitBlueprint + read_only_fields = ['id', + 'name', + 'description', + 'status', + 'unschedulable_reason', + 'error_reason', + 'placed', + 'scheduler', + 'scheduling_constraints_template_id', + 'scheduled_start_time', + 'scheduled_stop_time', + 'on_sky_start_time', + 'on_sky_stop_time', + 'duration', + 'on_sky_duration', + 'project', + 'antenna_set', + 'stations', + 'used_stations'] + fields = read_only_fields + + def create(self, validated_data): + raise NotImplementedError("This serializer should only be used for fast querying existing scheduling units") + + def update(self, instance, validated_data): + raise NotImplementedError("This serializer should only be used for fast querying existing scheduling units") + + class TaskDraftSerializer(DynamicRelationalHyperlinkedModelSerializer): duration = FloatDurationField(read_only=True) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index 37db5cfda230c217a46bab7c1c328033582abe84..5906a898d156ab56da522877128ef62baa655a65 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -269,6 +269,12 @@ class ReservationStrategyTemplateViewSet(AbstractTemplateViewSet): else: project = None + stations = request.query_params.get('stations', None) + if isinstance(stations, str): + stations = [s.strip() for s in stations.split(',')] + if stations: + reservation_template_spec['resources']['stations'] = stations + reservation = Reservation.objects.create(name=request.query_params.get('name', "reservation"), description=request.query_params.get('description', ""), project=project, @@ -913,6 +919,39 @@ class ModelChoiceInFilter(filters.BaseInFilter, filters.ModelChoiceFilter): """ pass +class SchedulingUnitBlueprintSlimPropertyFilter(property_filters.PropertyFilterSet): + '''A small 'slim' PropertyFilterSet for the most relevant properties.''' + on_sky_start_time = filters.IsoDateTimeFromToRangeFilter(field_name='on_sky_start_time') + on_sky_stop_time = filters.IsoDateTimeFromToRangeFilter(field_name='on_sky_stop_time') + scheduled_start_time = filters.IsoDateTimeFromToRangeFilter(field_name='scheduled_start_time') + scheduled_stop_time = filters.IsoDateTimeFromToRangeFilter(field_name='scheduled_stop_time') + project = filters.CharFilter(field_name='draft__scheduling_set__project__name', lookup_expr='icontains') + status = filters.MultipleChoiceFilter(field_name='status', choices=[(c.value, c.value) for c in models.SchedulingUnitStatus.Choices]) + 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 = filters.DurationFilter(field_name='duration', lookup_expr='gte') + duration_max = filters.DurationFilter(field_name='duration', lookup_expr='lte') + on_sky_duration_min = filters.DurationFilter(field_name='on_sky_duration', lookup_expr='gte') + on_sky_duration_max = filters.DurationFilter(field_name='on_sky_duration', lookup_expr='lte') + observed_duration_min = filters.DurationFilter(field_name='observed_duration', lookup_expr='gte') + observed_duration_max = filters.DurationFilter(field_name='observed_duration', lookup_expr='lte') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + + class Meta: + model = models.SchedulingUnitBlueprint + fields = '__all__' + filter_overrides = FILTER_OVERRIDES + + +class SchedulingUnitBlueprintSlimViewSet(LOFARViewSet): + '''A small 'slim' viewset for the most relevant properties.''' + queryset = models.SchedulingUnitBlueprint.objects.all() + serializer_class = serializers.SchedulingUnitBlueprintSlimSerializer + filter_class = SchedulingUnitBlueprintSlimPropertyFilter + + class SchedulingUnitBlueprintPropertyFilter(property_filters.PropertyFilterSet): on_sky_start_time = filters.IsoDateTimeFromToRangeFilter(field_name='on_sky_start_time') on_sky_stop_time = filters.IsoDateTimeFromToRangeFilter(field_name='on_sky_stop_time') diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index 8e843ab859ebf8dd4d4d5316f24edbabd071a032..6ae58bcb4628ebec445f2b7c5d877012fba572bf 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -178,6 +178,7 @@ router.register(r'scheduling_set', viewsets.SchedulingSetViewSet) router.register(r'scheduling_unit_draft_extended', viewsets.SchedulingUnitDraftExtendedViewSet) router.register(r'scheduling_unit_draft', viewsets.SchedulingUnitDraftViewSet) # ! The last registered view on a model is used for references to objects router.register(r'scheduling_unit_blueprint_extended', viewsets.SchedulingUnitBlueprintExtendedViewSet) +router.register(r'scheduling_unit_blueprint_slim', viewsets.SchedulingUnitBlueprintSlimViewSet) router.register(r'scheduling_unit_blueprint', viewsets.SchedulingUnitBlueprintViewSet) # ! The last registered view on a model is used for references to objects router.register(r'task_draft_extended', viewsets.TaskDraftExtendedViewSet) router.register(r'task_draft', viewsets.TaskDraftViewSet) diff --git a/SAS/TMSS/client/lib/tmss_http_rest_client.py b/SAS/TMSS/client/lib/tmss_http_rest_client.py index 9162fd2dd2acb00aaa2a3a69db368a1ffd720478..6e2a05343d1b69f6ae0b81b95e5c19ff65a5da8e 100644 --- a/SAS/TMSS/client/lib/tmss_http_rest_client.py +++ b/SAS/TMSS/client/lib/tmss_http_rest_client.py @@ -450,6 +450,10 @@ class TMSSsession(object): template = self.get_dataproduct_specifications_template(name=name, version=version) return self.get_url_as_json_object(template['url']+"/default") + def get_common_schema_template(self, name: str, version: int=None) -> dict: + '''get the common_schema_template as dict for the given name (and version)''' + return self._get_schema_template('common_schema_template', name, version) + def get_subtask_output_dataproducts(self, subtask_id: int) -> []: '''get the output dataproducts of the subtask with the given subtask_id''' return self.get_path_as_json_object('subtask/%s/output_dataproducts' % subtask_id) @@ -804,7 +808,8 @@ class TMSSsession(object): def create_reservation_for_strategy_template(self, start_time: datetime, stop_time: datetime=None, strategy_name: str='', strategy_version: int = None, project: str=None, - reservation_name: str=None, reservation_description: str=None) -> dict: + reservation_name: str=None, reservation_description: str=None, + stations: list=None) -> dict: '''create a reservation based on the reservation_strategy_template given by the strategy_name (and strategy_version), as dict for the given name (and version)''' template = self._get_strategy_template('reservation_strategy_template', strategy_name, strategy_version) @@ -817,6 +822,8 @@ class TMSSsession(object): params['name'] = reservation_name if reservation_description: params['description'] = reservation_description + if stations: + params['stations'] = stations if isinstance(stations, str) else ','.join(str(s) for s in stations) return self.do_request_and_get_result_as_json_object('POST', template['url'].rstrip('/')+'/create_reservation', params=params) def _get_strategy_template(self, template_type_name:str, name: str, version: int=None) -> dict: diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/data/week.view.data.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/data/week.view.data.js index bcec1a5b4858fb8a1fe0e00ed3f226462c4efa2f..d3041f90615a4ea68c3d0d35f885a7d06df475cd 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/data/week.view.data.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/data/week.view.data.js @@ -10,6 +10,7 @@ import AuthUtil from "../../../utils/auth.util"; import UtilService from "../../../services/util.service"; import {getSunTimingItems} from "../helpers/timeline.item.helper"; import moment from "moment/moment"; +import {getStore} from "../../../services/store.helper"; export async function fetchTimelineData(startTime, endTime, timelineCommonUtils, setData) { const from = startTime.format(UIConstants.CALENDAR_DATETIME_FORMAT); @@ -18,13 +19,22 @@ export async function fetchTimelineData(startTime, endTime, timelineCommonUtils, let schedulingUnits = await ScheduleService.getExpandedSUListConcise(from, until); schedulingUnits = enhanceSchedulingUnitData(schedulingUnits, timelineCommonUtils); - let reservations = await ReservationService.getTimelineReservations(from, until); - reservations = enhanceReservations(reservations, timelineCommonUtils) + const timelineStore = getStore(UIConstants.STORE_KEY_TIMELINE) - setData({ - schedulingUnits: schedulingUnits, - reservations: reservations - }) + if (timelineStore.reservationsToggle) { + let reservations = await ReservationService.getTimelineReservations(from, until); + reservations = enhanceReservations(reservations, timelineCommonUtils) + + setData({ + schedulingUnits: schedulingUnits, + reservations: reservations + }) + } else { + setData({ + schedulingUnits: schedulingUnits, + reservations: [] + }) + } } export async function fetchUserPermissions(setPermissions) { diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/week.view.helper.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/week.view.helper.js index 9dd4138d5063c2925e763750a2edbe34dabb2514..e2303feaa8a444d342b09abd1ae858283707e158 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/week.view.helper.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/helpers/week.view.helper.js @@ -158,12 +158,16 @@ export function getPageHeaderOptionsMenuItems(permissions) { } function getAntennaSet(su) { - for (let task of su.task_blueprints) { - const taskIsObservation = task.specifications_template.type_value.toLowerCase() === "observation"; - if (taskIsObservation) { - const stationConfiguration = task.specifications_doc.station_configuration; - if (stationConfiguration) { - su.antennaSet = stationConfiguration.antenna_set; + if(su.antenna_set) { + su.antennaSet = su.antenna_set; + } else { + for (let task of su.task_blueprints) { + const taskIsObservation = task.specifications_template.type_value.toLowerCase() === "observation"; + if (taskIsObservation) { + const stationConfiguration = task.specifications_doc.station_configuration; + if (stationConfiguration) { + su.antennaSet = stationConfiguration.antenna_set; + } } } } @@ -184,10 +188,9 @@ function getStationsInfo(timelineCommonUtils, stations) { export function enhanceSchedulingUnitData(schedulingUnits, timelineCommonUtils) { return schedulingUnits.map(su => { - su.project = su.draft?.scheduling_set?.project?.name; - su.scheduleMethod = su.scheduling_constraints_doc.scheduler + su.scheduleMethod = su.scheduler; getAntennaSet(su); - su.stations = getStationsInfo(timelineCommonUtils, timelineCommonUtils.getSUStations(su)); + su.stations = getStationsInfo(timelineCommonUtils, su.stations); return su }) } diff --git a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js index 5aaccd0d3888478d1b4cad877ffb7bfd071e3cc4..b28e25114da1fe68455a118a4a47feabd62bfe30 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/services/schedule.service.js @@ -165,8 +165,8 @@ const ScheduleService = { getExpandedSUListConcise: async (startTime, endTime) => { let blueprints = []; try { - let url = `/api/scheduling_unit_blueprint/?ordering=name&expand=${SU_EXPAND_FIELDS_CONCISE.join()}&fields=${SU_FETCH_FIELDS_CONCISE.join()}`; - url = `${url}&scheduled_start_time_before=${endTime || ''}&scheduled_stop_time_after=${startTime || ''}`; + let url = `/api/scheduling_unit_blueprint_slim/`; + url = `${url}?scheduled_start_time_before=${endTime || ''}&scheduled_stop_time_after=${startTime || ''}`; let initialResponse = await axios.get(url); const totalCount = initialResponse.data.count; const initialCount = initialResponse.data.results.length diff --git a/SAS/TMSS/scripts/CMakeLists.txt b/SAS/TMSS/scripts/CMakeLists.txt index 4e21e183b728333091ef29ba300fa245d91fb9e2..419c244c30f3b65b3ae729d8acb3ba1f1148741b 100644 --- a/SAS/TMSS/scripts/CMakeLists.txt +++ b/SAS/TMSS/scripts/CMakeLists.txt @@ -1,5 +1,6 @@ lofar_package(TMSSScripts 0.1 DEPENDS TMSSClient) lofar_add_bin_scripts(tmss_create_idols_scheduling_units) +lofar_add_bin_scripts(tmss_create_standalone_reservations) lofar_add_bin_scripts(tmss_make_unused_legacy_templates_obsolete) diff --git a/SAS/TMSS/scripts/tmss_create_standalone_reservations b/SAS/TMSS/scripts/tmss_create_standalone_reservations new file mode 100755 index 0000000000000000000000000000000000000000..6dfe23da81a639e833cee40fda00ec6ea7340b9d --- /dev/null +++ b/SAS/TMSS/scripts/tmss_create_standalone_reservations @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from datetime import datetime, date, timedelta, time +from lofar.sas.tmss.client.tmss_http_rest_client import TMSSsession + +# by default, we start creating the reservation(s) for next tuesday and onwards. +today = date.today() +tuesday = today + timedelta(days=1-today.weekday()) # tuesday is weekday=1 +next_tuesday = tuesday + timedelta(days=7) if tuesday < today else tuesday + +parser = argparse.ArgumentParser(description="Create 1 (default) or more reservation(s) for the Standalone-mode for international LOFAR station(s).", add_help=True) +parser.add_argument("-c", "--count", + type=int, + help="create this number of reservations (one per week, starting at --start_date), default: 1", + default=1) +parser.add_argument('-s', '--start_date', + help="create the first upcoming standalone reservation (of the given --nr_of_units) starting at this date (format=\"YYYY-MM-DD\"), default=\"%s\""%(next_tuesday.strftime("%Y-%m-%d"),), + default=next_tuesday.strftime("%Y-%m-%d")) +parser.add_argument('-t', '--time', + help="starttime of the reservation(s) (format=\"HH:MM\" in UTC), default=\"06:45\"\n" + "guideline 06:45 in summer, 07:45 in winter. 15min to switch, station is handed over at the hour.", + default="06:45") +parser.add_argument('-d', '--duration', + help="the duration (in hours) of the reservation. Default is 32hr: 15min switching at start, +31hr standalone time, +45min switching at end", + type=float, + default=32) +parser.add_argument('-S', '--stations', + help="specify stations to reserve, either by group name, or as comma-seperated-station-names. Default: \"International\"", + default="International") +parser.add_argument('-R', '--rest_api_credentials', type=str, + help='TMSS django REST API credentials name, default: TMSSClient', + default='TMSSClient') +args = parser.parse_args() + + +with TMSSsession.create_from_dbcreds_for_ldap(args.rest_api_credentials) as client: + for counter in range(args.count): + start_timestamp = datetime.strptime(args.start_date + " " + args.time, "%Y-%m-%d %H:%M") + timedelta(days=7*counter) + stop_timestamp = start_timestamp + timedelta(hours=args.duration) + + if args.stations != "International" and ',' not in args.stations: + stations_schema = client.get_common_schema_template('stations') + groups = stations_schema['schema']['definitions']['station_group']['anyOf'] + try: + selected_group = next(g for g in groups if g['title'].lower() == args.stations.lower()) + stations = selected_group['properties']['stations']['enum'][0] + except StopIteration: + print("No such station group: '%s'" % (args.stations,)) + print("Available station groups: %s" % (', '.join([g['title'] for g in groups]),)) + exit(1) + elif ',' in args.stations: + stations = [s.strip() for s in args.stations.split(',')] + else: + # defaults from the template: International + stations = None + + reservation = client.create_reservation_for_strategy_template(start_time=start_timestamp, + stop_time=stop_timestamp, + reservation_name="ILT stations in local mode", + strategy_name="ILT stations in local mode", + stations=stations) + + # adapt api url to frontend view url, and print results + view_url = reservation['url'].replace('api/reservation/', 'reservation/view/') + print("created reservation id=%d name='%s' url: %s" % (reservation['id'], reservation['name'], view_url)) +