From ce9a24fb700707414f7730b67462130beb9a9ba7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20K=C3=BCnsem=C3=B6ller?=
 <jkuensem@physik.uni-bielefeld.de>
Date: Mon, 3 May 2021 18:53:50 +0200
Subject: [PATCH] TMSS-703: Cancel running obs on triggered obs, add project
 property to subtask, modify path_to_project to deal with ManyToMany fields

---
 .../backend/src/tmss/tmssapp/models/common.py | 27 ++++++++-
 .../src/tmss/tmssapp/models/scheduling.py     |  5 +-
 .../src/tmss/tmssapp/models/specification.py  | 57 ++++++++-----------
 3 files changed, 53 insertions(+), 36 deletions(-)

diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
index 4eeeb68e1a4..f9245411fe4 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py
@@ -14,6 +14,12 @@ from django.urls import reverse as reverse_url
 import json
 import jsonschema
 from datetime import timedelta
+from django.utils.functional import cached_property
+from lofar.sas.tmss.tmss.exceptions import TMSSException
+
+#
+# Mixins
+#
 
 class RefreshFromDbInvalidatesCachedPropertiesMixin():
     """Helper Mixin class which invalidates all 'cached_property' attributes on a model upon refreshing from the db"""
@@ -22,11 +28,30 @@ class RefreshFromDbInvalidatesCachedPropertiesMixin():
         return super().refresh_from_db(*args, **kwargs)
 
     def invalidate_cached_properties(self):
-        from django.utils.functional import cached_property
         for key, value in self.__class__.__dict__.items():
             if isinstance(value, cached_property):
                 self.__dict__.pop(key, None)
 
+
+class ProjectPropertyMixin(RefreshFromDbInvalidatesCachedPropertiesMixin):
+    @cached_property
+    def project(self): # -> Project:
+        '''return the related project of this task
+        '''
+        if not hasattr(self, 'path_to_project'):
+            return TMSSException("Please define a 'path_to_project' attribute on the %s object for the ProjectPropertyMixin to function." % type(self))
+        obj = self
+        for attr in self.path_to_project.split('__'):
+            obj = getattr(obj, attr)
+            if attr == 'project':
+                return obj
+            if obj and not isinstance(obj, Model):  # ManyToMany fields
+                obj = obj.first()
+            if obj is None:
+                logger.warning("The element '%s' in the path_to_project of the %s object returned None for pk=%s" % (attr, type(self), self.pk))
+                return None
+
+
 # abstract models
 
 class BasicCommon(Model):
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
index 3fa4cc2134a..82179139a3f 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
@@ -12,7 +12,7 @@ from django.db.models import Model, ForeignKey, OneToOneField, CharField, DateTi
     ManyToManyField, CASCADE, SET_NULL, PROTECT, QuerySet, BigAutoField, UniqueConstraint
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.contrib.auth.models import User
-from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin
+from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, ProjectPropertyMixin
 from enum import Enum
 from django.db.models.expressions import RawSQL
 from django.core.exceptions import ValidationError
@@ -138,7 +138,7 @@ class SIPidentifier(Model):
 #
 # Instance Objects
 #
-class Subtask(BasicCommon, TemplateSchemaMixin):
+class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin):
     """
     Represents a low-level task, which is an atomic unit of execution, such as running an observation, running
     inspection plots on the observed data, etc. Each task has a specific configuration, will have resources allocated
@@ -156,6 +156,7 @@ class Subtask(BasicCommon, TemplateSchemaMixin):
     created_or_updated_by_user = ForeignKey(User, null=True, editable=False, on_delete=PROTECT, help_text='The user who created / updated the subtask.')
     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_blueprints__scheduling_unit_blueprint__draft__scheduling_set__project'
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
index 68091010b7e..195957a49bb 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py
@@ -10,7 +10,7 @@ from django.contrib.postgres.fields import JSONField
 from enum import Enum
 from django.db.models.expressions import RawSQL
 from django.db.models.deletion import ProtectedError
-from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, NamedCommonPK, RefreshFromDbInvalidatesCachedPropertiesMixin
+from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, NamedCommonPK, RefreshFromDbInvalidatesCachedPropertiesMixin, ProjectPropertyMixin
 from lofar.common.json_utils import validate_json_against_schema, validate_json_against_its_schema, add_defaults_to_json_object_for_schema
 from lofar.sas.tmss.tmss.exceptions import *
 from django.core.exceptions import ValidationError
@@ -19,24 +19,6 @@ from collections import Counter
 from django.utils.functional import cached_property
 
 
-#
-# Mixins
-#
-
-class ProjectPropertyMixin(RefreshFromDbInvalidatesCachedPropertiesMixin):
-    @cached_property
-    def project(self): # -> Project:
-        '''return the related project of this task
-        '''
-        if not hasattr(self, 'path_to_project'):
-            return TMSSException("Please define a 'path_to_project' attribute on the object for the ProjectPropertyMixin to function.")
-        obj = self
-        for attr in self.path_to_project.split('__'):
-            obj = getattr(obj, attr)
-            if attr == 'project':
-                return obj
-
-
 #
 # I/O
 #
@@ -497,6 +479,29 @@ class SchedulingUnitBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, Tem
     def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
         self.annotate_validate_add_defaults_to_doc_using_template('requirements_doc', 'requirements_template')
 
+        if self._state.adding and self.is_triggered and self.project.can_trigger:
+            from lofar.sas.tmss.services.scheduling.constraints import can_run_after
+            start_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=3)
+            logger.info('Checking if triggered obs name=%s can run after start_time=%s' % (self.name, start_time))
+            if self.draft.scheduling_constraints_template is None or can_run_after(self, start_time):  # todo: use self.scheduling_constraints_template after TMSS-697 was merged
+                from lofar.sas.tmss.tmss.tmssapp.subtasks import cancel_subtask
+                from lofar.sas.tmss.tmss.tmssapp.models import Subtask, SubtaskState, SubtaskType
+                current_obs_subtasks = Subtask.objects.filter(state__value__in=[SubtaskState.Choices.STARTING.value, SubtaskState.Choices.STARTED.value],
+                                                              specifications_template__type__value=SubtaskType.Choices.OBSERVATION.value).all()
+                if len(current_obs_subtasks) == 0:
+                    logger.info('No currently running observation detected, so nothing to cancel for triggered obs name=%s' % self.name)
+                for current_obs_subtask in current_obs_subtasks:
+                    if self.project is None:
+                        logger.warning('Triggered obs name=%s cannot cancel running subtask obs pk=%s because it does not belong to a project and hence has unknown priority' % (self.name, current_obs_subtask.pk))
+                        continue
+                    if current_obs_subtask.project is None:
+                        logger.warning('Triggered obs name=%s priority=% cannot cancel running subtask obs pk=%s because the running obs does not belong to a project and hence has unknown priority' % (self.name, self.project.trigger_priority, current_obs_subtask.pk))
+                        continue
+                    logger.info('Checking if triggered obs name=%s priority=%s should cancel running subtask obs pk=%s priority=%s' % (self.name, self.project.trigger_priority, current_obs_subtask.pk, current_obs_subtask.project.trigger_priority))
+                    if self.project.trigger_priority > current_obs_subtask.project.trigger_priority:
+                        logger.info('Cancelling running obs subtask pk=%s for triggered obs name=%s' % (current_obs_subtask.pk, self.name))
+                        cancel_subtask(current_obs_subtask)
+
         # This code only happens if the objects is not in the database yet. self._state.adding is True creating
         if self._state.adding and hasattr(self, 'draft'):
             self.ingest_permission_required = self.draft.ingest_permission_required
@@ -728,20 +733,6 @@ class SchedulingUnitBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, Tem
         return fields_found
 
 
-class ProjectPropertyMixin():
-    @cached_property
-    def project(self) -> Project:
-        '''return the related project of this task
-        '''
-        if not hasattr(self, 'path_to_project'):
-            return TMSSException("Please define a 'path_to_project' attribute on the object for the ProjectPropertyMixin to function.")
-        obj = self
-        for attr in self.path_to_project.split('__'):
-            obj = getattr(obj, attr)
-            if attr == 'project':
-                return obj
-
-
 class TaskDraft(NamedCommon, ProjectPropertyMixin, TemplateSchemaMixin):
     specifications_doc = JSONField(help_text='Specifications for this task.')
     copies = ForeignKey('TaskDraft', related_name="copied_from", on_delete=SET_NULL, null=True, help_text='Source reference, if we are a copy (NULLable).')
-- 
GitLab