diff --git a/SAS/TMSS/backend/src/CMakeLists.txt b/SAS/TMSS/backend/src/CMakeLists.txt index f80c85dc26bfdd8d5a769f107b6b4e0830c00016..167c3911f70d60354827352a249b0b3c8fc75394 100644 --- a/SAS/TMSS/backend/src/CMakeLists.txt +++ b/SAS/TMSS/backend/src/CMakeLists.txt @@ -32,7 +32,6 @@ find_python_module(swagger_spec_validator REQUIRED) # pip3 install swagger-spec- set(_py_files manage.py - remakemigrations.py ) python_install(${_py_files} diff --git a/SAS/TMSS/backend/src/remakemigrations.py b/SAS/TMSS/backend/src/remakemigrations.py deleted file mode 100755 index ceb04af2b7220d8f40cc0ebb7c75d17b8edd48dd..0000000000000000000000000000000000000000 --- a/SAS/TMSS/backend/src/remakemigrations.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2018 ASTRON (Netherlands Institute for Radio Astronomy) -# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This file is part of the LOFAR software suite. -# The LOFAR software suite is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# The LOFAR software suite is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. - -# $Id: $ - -# This script automates the procedure to replace the existing migrations on the source tree with initital migrations -# based on the current datamodel. Django offers a call 'makemigrations' through manage.py, which creates new migrations -# after the datamodel implementation has changed. These additional migrations apply those changes to an existing -# database reflecting the previous datamodel. -# This is a very nice feature for production, but there are a few downsides that this script tackles: -# -# 1. During development, where the datamodel constantly changes, whe typically don't want a ton of iterative migrations, -# but just have a clean start with a fresh initial database state without the whole provenance is perfectly fine. (We -# start up a fresh database anyway for every test or test deployment.) This can be achieved by removing all existing -# migrations prior to creating new ones. -# A difficulty with this approach is that we do have a manual migration to populate the database with fixtures. -# This migration needs to be restored or re-created after Django created fresh migrations for the database itself. -# -# 2. Since in settings.py we refer to the tmss app in the lofar environment, Django uses the build or installed version. -# A consequence is that the created migrations are placed in there and need to be copied to the source tree. -# -# This script requires a running postgres database instance to work against. -# To use specific database credentials, run e.g. ./remakemigrations.py -C b5f881c4-d41a-4f24-b9f5-23cd6a7f37d0 - - -import os -from glob import glob -import subprocess as sp -import logging -import argparse -from shutil import copy -import lofar.sas.tmss - -logger = logging.getLogger(__file__) - -# set up paths -tmss_source_directory = os.path.dirname(__file__) -if tmss_source_directory == '': - tmss_source_directory = '.' -tmss_env_directory = os.path.dirname(lofar.sas.tmss.__file__) -relative_migrations_directory = '/tmss/tmssapp/migrations/' - -# template for manual changes and fixture (applied last): -template = """ -# -# auto-generated by remakemigrations.py -# -# ! Please make sure to apply any changes to the template in that script ! -# -from django.db import migrations - -from lofar.sas.tmss.tmss.tmssapp.populate import * - -class Migration(migrations.Migration): - - dependencies = [ - ('tmssapp', '{migration_dependency}'), - ] - - operations = [ migrations.RunSQL('ALTER SEQUENCE tmssapp_SubTask_id_seq RESTART WITH 2000000;'), # Start SubTask id with 2 000 000 to avoid overlap with 'old' (test/production) OTDB - # add an SQL trigger in the database enforcing correct state transitions. - # it is crucial that illegal subtask state transitions are block at the "lowest level" (i.e.: in the database) so we can guarantee that the subtask state machine never breaks. - # see: https://support.astron.nl/confluence/display/TMSS/Subtask+State+Machine - # Explanation of SQl below: A trigger function is called upon each create/update of the subtask. - # If the state changes, then it is checked if the state transition from old to new is present in the SubtaskAllowedStateTransitions table. - # If not an Exception is raised, thus enforcing a rollback, thus enforcing the state machine to follow the design. - # It is thereby enforced upon the user/caller to handle these blocked illegal state transitions, and act more wisely. - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_check_subtask_state_transition() - RETURNS trigger AS - $BODY$ - BEGIN - IF TG_OP = 'INSERT' THEN - IF NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id IS NULL AND new_state_id=NEW.state_id)) THEN - RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM % TO %', NULL, NEW.state_id; - END IF; - END IF; - IF TG_OP = 'UPDATE' THEN - IF OLD.state_id <> NEW.state_id AND NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id=OLD.state_id AND new_state_id=NEW.state_id)) THEN - RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM "%" TO "%"', OLD.state_id, NEW.state_id; - END IF; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_on_check_subtask_state_transition ON tmssapp_SubTask ; - CREATE TRIGGER tmssapp_trigger_on_check_subtask_state_transition - BEFORE INSERT OR UPDATE ON tmssapp_SubTask - FOR EACH ROW EXECUTE PROCEDURE tmssapp_check_subtask_state_transition();'''), - - # use database triggers to block updates on blueprint tables for immutable fields - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_scheduling_unit_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.name <> NEW.name OR - OLD.description <> NEW.description THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_scheduling_unit_blueprint_immutable_fields_update ON tmssapp_SchedulingUnitBlueprint ; - CREATE TRIGGER tmssapp_block_scheduling_unit_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_SchedulingUnitBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_scheduling_unit_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_task_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_doc <> NEW.specifications_doc OR - OLD.name <> NEW.name OR - OLD.description <> NEW.description OR - OLD.short_description <> NEW.short_description OR - OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.scheduling_unit_blueprint_id <> NEW.scheduling_unit_blueprint_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_task_blueprint_immutable_fields_update ON tmssapp_TaskBlueprint ; - CREATE TRIGGER tmssapp_block_task_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_TaskBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_task_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_task_relation_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.selection_doc <> NEW.selection_doc OR - OLD.producer_id <> NEW.producer_id OR - OLD.consumer_id <> NEW.consumer_id OR - OLD.input_role_id <> NEW.input_role_id OR - OLD.output_role_id <> NEW.output_role_id OR - OLD.input_role_id <> NEW.input_role_id OR - OLD.selection_template_id <> NEW.selection_template_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_task_relation_blueprint_immutable_fields_update ON tmssapp_TaskRelationBlueprint ; - CREATE TRIGGER tmssapp_block_task_relation_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_TaskRelationBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_task_relation_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_subtask_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_doc <> NEW.specifications_doc OR - OLD.primary <> NEW.primary OR - OLD.task_blueprint_id <> NEW.task_blueprint_id OR - OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.cluster_id <> NEW.cluster_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE SUBTASK FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_subtask_immutable_fields_update ON tmssapp_Subtask ; - CREATE TRIGGER tmssapp_block_subtask_immutable_fields_update - BEFORE UPDATE ON tmssapp_Subtask - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_subtask_immutable_fields_update(); - '''), - migrations.RunPython(populate_choices), - migrations.RunPython(populate_subtask_allowed_state_transitions), - migrations.RunPython(populate_settings), - migrations.RunPython(populate_misc), - migrations.RunPython(populate_resources), - migrations.RunPython(populate_cycles), - migrations.RunPython(populate_projects) ] - -""" - - -def execute_and_log(cmd): - - logger.info('COMMAND: %s' % cmd) - p = sp.Popen(cmd, shell=True, stdout=sp.PIPE, stderr=sp.PIPE) - out, err = p.communicate() - if out is not None: - logger.info("STDOUT: %s" % out.decode('utf-8').strip()) - if err is not None: - logger.info("STDERR: %s" % err.decode('utf-8').strip()) - - -def delete_old_migrations(): - logger.info('Removing old migrations...') - - files = glob_migrations() - for f in [path for path in files if ("initial" in path or "auto" in path or "populate" in path)]: - logger.info('Deleting: %s' % f) - os.remove(f) - - -def make_django_migrations(dbcredentials=None): - - logger.info('Making Django migrations...') - if dbcredentials: - os.environ['TMSS_DBCREDENTIALS'] = dbcredentials - execute_and_log('/usr/bin/env python3 %s/manage.py makemigrations' % tmss_source_directory) - - -def make_populate_migration(): - - logger.info('Making migration for populating database...') - last_migration = determine_last_migration() - migration = template.format(migration_dependency=last_migration) - - path = tmss_env_directory + relative_migrations_directory + '%s_populate.py' % str(int(last_migration.split('_')[0])+1).zfill(4) - logger.info('Writing to: %s' % path) - with open(path,'w') as f: - f.write(migration) - - -def glob_migrations(directories=(tmss_source_directory, tmss_env_directory)): - paths = [] - for directory in directories: - paths += glob(directory + '/' + relative_migrations_directory + '0*_*') - return paths - - -def copy_migrations_to_source(): - logger.info('Copying over migrations to source directory...') - files = glob_migrations(directories=[tmss_env_directory]) - for file in files: - logger.info('Copying %s to %s' % (file, tmss_source_directory + '/' + relative_migrations_directory)) - copy(file, tmss_source_directory + '/' + relative_migrations_directory) - - -def determine_last_migration(): - logger.info('Determining last migration...') - files = glob_migrations() - files = [os.path.basename(path) for path in files] - f = max(files) - last_migration = f.split('.py')[0] - logger.info('Determined last migration: %s' % last_migration) - return last_migration - - -def remake_migrations(dbcredentials=None): - delete_old_migrations() - make_django_migrations(dbcredentials) - make_populate_migration() - copy_migrations_to_source() - - -if __name__ == "__main__": - - logger.setLevel(logging.DEBUG) - - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - parser = argparse.ArgumentParser() - parser.add_argument("-C", action="store", dest="dbcredentials", help="use database specified in these dbcredentials") - args = parser.parse_args() - remake_migrations(args.dbcredentials) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py new file mode 100644 index 0000000000000000000000000000000000000000..a784361a44d64cb70e4f95effc9cc722dbeb8ef0 --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py @@ -0,0 +1,55 @@ +# Generated by Django 3.0.9 on 2021-10-15 07:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0003_taskblueprint_shortdescription'), + ] + + operations = [ + migrations.AddField( + model_name='subtask', + name='error_reason', + field=models.CharField(help_text='Reason why the Subtask went to error.', max_length=200, null=True), + ), + migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_subtask_immutable_fields_update() + RETURNS trigger AS + $BODY$ + BEGIN + IF TG_OP = 'UPDATE' THEN + IF OLD.specifications_doc <> NEW.specifications_doc OR + OLD.primary <> NEW.primary OR + OLD.task_blueprint_id <> NEW.task_blueprint_id OR + OLD.specifications_template_id <> NEW.specifications_template_id OR + OLD.cluster_id <> NEW.cluster_id THEN + RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE SUBTASK FIELD'; + END IF; + + IF OLD.error_reason <> NEW.error_reason OR (OLD.error_reason IS NULL AND NEW.error_reason IS NOT NULL) THEN + IF OLD.error_reason IS NOT NULL THEN + RAISE EXCEPTION 'subtask.error_reason may only be set once'; + END IF; + IF NEW.error_reason IS NOT NULL AND NEW.state_id <> 'error' THEN + RAISE EXCEPTION 'subtask.error_reason may only be set when state==error'; + END IF; + END IF; + END IF; + + IF TG_OP = 'INSERT' THEN + IF NEW.error_reason IS NOT NULL AND NEW.state_id <> 'error' THEN + RAISE EXCEPTION 'subtask.error_reason may only be set when state==error'; + END IF; + END IF; + RETURN NEW; + END; + $BODY$ + LANGUAGE plpgsql VOLATILE; + DROP TRIGGER IF EXISTS tmssapp_block_subtask_immutable_fields_update ON tmssapp_Subtask ; + CREATE TRIGGER tmssapp_block_subtask_immutable_fields_update + BEFORE UPDATE OR INSERT ON tmssapp_Subtask + FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_subtask_immutable_fields_update(); + '''), + ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index 7b3200b15cc433f0feaa38cb77c72c8927bcd279..f0a92108015c5d6de0e65b7f415b876ecd25bc63 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -152,6 +152,7 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): cluster = ForeignKey('Cluster', null=True, on_delete=PROTECT, help_text='Where the Subtask is scheduled to run (NULLable).') # resource_claim = ForeignKey("ResourceClaim", null=False, on_delete=PROTECT) # todo <-- how is this external reference supposed to work? created_or_updated_by_user = ForeignKey(User, null=True, editable=False, on_delete=PROTECT, help_text='The user who created / updated the subtask.') + error_reason = CharField(null=True, max_length=200, help_text='Reason why the Subtask went to error.') raw_feedback = CharField(null=True, max_length=1048576, help_text='The raw feedback for this Subtask') global_identifier = OneToOneField('SIPidentifier', null=False, editable=False, on_delete=PROTECT, help_text='The global unique identifier for LTA SIP.') path_to_project = 'task_blueprint__scheduling_unit_blueprint__draft__scheduling_set__project' diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py index 72c0cf92be4ea0be4c810804f9cb2443e757667c..92746b6c3c340e7b90ed9683c8d60c5e033c1f80 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py @@ -6,6 +6,7 @@ import logging logger = logging.getLogger(__name__) from rest_framework import serializers +from rest_framework.exceptions import ValidationError from .. import models from .widgets import JSONEditorField from .common import FloatDurationField, RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer @@ -74,7 +75,6 @@ class SubtaskSerializer(DynamicRelationalHyperlinkedModelSerializer): input_dataproducts = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='dataproduct-detail') output_dataproducts = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='dataproduct-detail') - class Meta: model = models.Subtask fields = '__all__' diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py index ba36e9feff0c0990626f04413ec4d18c5a94b706..5a314a43ba3841a82c07079f0d44ae0c7db31c9e 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py @@ -983,8 +983,9 @@ def schedule_subtask(subtask: Subtask) -> Subtask: subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value) subtask.save() else: - # set the subtask to state 'ERROR'. TODO: we should annotate in the db what error occurred. + # set the subtask to state 'ERROR'. subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + subtask.error_reason = f'{e}' subtask.save() except Exception as e2: logger.error(e2) @@ -2290,6 +2291,7 @@ def cancel_subtask(subtask: Subtask) -> Subtask: except Exception as e: logger.error("Error while cancelling subtask id=%s type=%s state=%s '%s'", subtask.id, subtask.specifications_template.type.value, subtask.state.value, e) subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + subtask.error_reason = f'{e}' subtask.save() if isinstance(e, SubtaskCancellingException): # we intentionally raised the SubtaskCancellingException, so re-raise it and let the caller handle it diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py index 5ab6c1e51a1a535c129b8b895f456bf046e20eba..c5adc99347792fe207ec34d8ff8efa0a331201e4 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py @@ -127,6 +127,7 @@ class SubTaskFilter(property_filters.PropertyFilterSet): id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') state = filters.ModelMultipleChoiceFilter(field_name='state', queryset=models.SubtaskState.objects.all()) name = filters.CharFilter(field_name='task_blueprint__scheduling_unit_blueprint__name', lookup_expr='icontains') # todo: correct name? + error_reason = filters.CharFilter(field_name='error_reason', lookup_expr='icontains') on_sky_start_time__lt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_start_time', lookup_expr='lt') on_sky_start_time__gt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_start_time', lookup_expr='gt') on_sky_stop_time__lt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_stop_time', lookup_expr='lt') diff --git a/SAS/TMSS/backend/test/t_subtasks.py b/SAS/TMSS/backend/test/t_subtasks.py index 9323947f44de942291823bbf2d8440085f532a4c..13d48f91803829d0950ba0c148be33b572ac72f3 100755 --- a/SAS/TMSS/backend/test/t_subtasks.py +++ b/SAS/TMSS/backend/test/t_subtasks.py @@ -754,8 +754,17 @@ class SubtaskAllowedStateTransitionsTest(unittest.TestCase): # then go to the error state (should be allowed from any of these intermediate states) subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + ERROR_MESSAGE = 'test_helper_method_set_subtask_state_following_allowed_transitions_error_path' + subtask.error_reason = ERROR_MESSAGE subtask.save() self.assertEqual(SubtaskState.Choices.ERROR.value, subtask.state.value) + self.assertEqual(ERROR_MESSAGE, subtask.error_reason) + + # overwriting error reason should not be allowed + from django.db.utils import InternalError + with self.assertRaises(InternalError) as context: + subtask.error_reason = "overwriting error reason" + subtask.save() def test_helper_method_set_subtask_state_following_allowed_transitions_cancel_path(self): for desired_end_state_value in (SubtaskState.Choices.CANCELLING.value,SubtaskState.Choices.CANCELLED.value): diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py index 63dc0bc8747294f21aa90d0a2f69b0adf374c4cf..3a0ca74d9d48e9bcff090432026020fbe7cf8e0a 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py @@ -739,6 +739,7 @@ class SchedulingUnitBlueprintTest(unittest.TestCase): task_blueprint = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(scheduling_unit_blueprint=scheduling_unit_blueprint)) subtask = models.Subtask.objects.create(**Subtask_test_data(task_blueprint=task_blueprint)) subtask.state = models.SubtaskState.objects.get(value='error') # the derived SUB status is then also error + subtask.error_reason = 'test_SchedulingUnitBlueprint_prevents_updating_scheduling_constraints_template_if_not_in_correct_state' subtask.save() scheduling_unit_blueprint.refresh_from_db() @@ -779,6 +780,7 @@ class SchedulingUnitBlueprintTest(unittest.TestCase): task_blueprint = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(scheduling_unit_blueprint=scheduling_unit_blueprint)) subtask = models.Subtask.objects.create(**Subtask_test_data(task_blueprint=task_blueprint)) subtask.state = models.SubtaskState.objects.get(value='error') # the derived SUB status is then also error + subtask.error_reason = 'test_SchedulingUnitBlueprint_prevents_updating_scheduling_constraints_doc_if_not_in_correct_state' subtask.save() scheduling_unit_blueprint.refresh_from_db() diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py index e22f2ecb6a37912694fa32e5b7626ac8e87d1c84..10af349b6eb2c4fd5fcc1b500514d6ab94749af6 100644 --- a/SAS/TMSS/backend/test/test_utils.py +++ b/SAS/TMSS/backend/test/test_utils.py @@ -111,6 +111,7 @@ def set_subtask_state_following_allowed_transitions(subtask: typing.Union[Subtas SubtaskState.Choices.FINISHING.value, SubtaskState.Choices.CANCELLING.value): subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + subtask.error_reason = 'set_subtask_state_following_allowed_transitions' # handle "unsuccessful path" to OBSOLETE end state (via CANCELLED) elif desired_state_value == SubtaskState.Choices.OBSOLETE.value: