Commits (2)
......@@ -5,6 +5,7 @@ This file contains the database models
from django.db.models import ForeignKey, CharField, DateTimeField, BooleanField, IntegerField, BigIntegerField, \
from django.contrib.postgres.fields import ArrayField, JSONField
from django.contrib.auth.models import User
from .specification import AbstractChoice, BasicCommon, Template, NamedCommon # , <TaskBlueprint
from enum import Enum
from rest_framework.serializers import HyperlinkedRelatedField
......@@ -95,6 +96,7 @@ class ScheduleMethod(AbstractChoice):
BATCH = 'batch'
DYNAMIC = 'dynamic'
# Templates
......@@ -153,10 +155,6 @@ class Subtask(BasicCommon):
scheduler_input_doc = JSONField(help_text='Partial specifications, as input for the scheduler.')
# resource_claim = ForeignKey("ResourceClaim", null=False, on_delete=PROTECT) # todo <-- how is this external reference supposed to work?
def __init__(self, *args, **kwargs):
super(Subtask, self).__init__(*args, **kwargs)
self.__original_state = self.state
def validate_specification_against_schema(self):
if self.specifications_doc is None or self.specifications_template_id is None:
......@@ -177,16 +175,21 @@ class Subtask(BasicCommon):
'''override of normal save method, doing a validation of the specification against the schema first
:raises SpecificationException in case the specification does not validate against the schema'''
if self.state != self.__original_state:
state_update = SubtaskStateLog(subtask=self, old_state=self.__original_state, new_state=self.state)
super().save(force_insert, force_update, using, update_fields)
class SubtaskStateLog(BasicCommon):
History of state changes on subtasks
This is now a very specific solution and based on what SOS communicated is what they are regularly interested in.
Maybe one or two additional log tables for other models are benefitial and should be added at some point.
Note that we could of course also log on the db level and there is also a variety of audit middlewares for Django
available to keep track of changes more generally: https://djangopackages.org/grids/g/model-audit/
This seems a bit overkill at the moment and we have to manage access to those logs etc., this needs tbd.
user = ForeignKey(User, null=False, editable=False, on_delete=PROTECT, help_text='The user who changed the state of the subtask.')
user_identifier = CharField(null=False, editable=False, max_length=128, help_text='The ID of the user who changed the state of the subtask.')
subtask = ForeignKey('Subtask', null=False, editable=False, on_delete=CASCADE, help_text='Subtask to which this state change refers.')
old_state = ForeignKey('SubtaskState', null=False, editable=False, on_delete=PROTECT, related_name='is_old_state_of', help_text='Subtask state before update (see Subtask State Machine).')
new_state = ForeignKey('SubtaskState', null=False, editable=False, on_delete=PROTECT, related_name='is_new_state_of', help_text='Subtask state after update (see Subtask State Machine).')
......@@ -265,3 +268,4 @@ class DataproductHash(BasicCommon):
dataproduct = ForeignKey('Dataproduct', on_delete=PROTECT, help_text='The dataproduct to which this hash refers.')
algorithm = ForeignKey('Algorithm', null=False, on_delete=PROTECT, help_text='Algorithm used (MD5, AES256).')
hash = CharField(max_length=128, help_text='Hash value.')
......@@ -171,11 +171,19 @@ class SubtaskSerializerJSONeditorOnline(RelationalHyperlinkedModelSerializer):
# todo: Shall we use one of the default templates for the init?
logger.warning('Could not determine schema, hence no fancy JSON form. Expected for list view.')
#def create(self, validated_data):
#subtaskstateupdate_serializer = SubtaskStateLogSerializer(validated_data.get('customer'))
# return User.objects.create(**validated_data)
# = self.context.get('request').user
# Intercept updates to also create a log entry
def update(self, instance, validated_data):
if instance.state is not None \
and validated_data.get('state') is not None \
and instance.state != validated_data.get('state'):
user = self.context.get('request').user
log_entry = models.SubtaskStateLog(user=user,
return super().update(instance, validated_data)
class Meta:
model = models.Subtask
......@@ -4,6 +4,7 @@ This file contains the serializers (for the elsewhere defined data models)
from rest_framework import serializers
from .. import models
from django.contrib.auth.models import User
from rest_framework import decorators
class RelationalHyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
......@@ -17,6 +18,14 @@ class RelationalHyperlinkedModelSerializer(serializers.HyperlinkedModelSerialize
return expanded_fields
# This is required for keeping a user reference as ForeignKey in other models
# (I think so that the HyperlinkedModelSerializer can generate a URI)
class UserSerializer(serializers.Serializer):
class Meta:
model = User
fields = '__all__'
class TagsSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.Tags
......@@ -43,6 +43,17 @@ class SubtaskStateViewSet(LOFARViewSet):
class SubtaskStateLogViewSet(LOFARViewSet):
def get_queryset(self):
queryset = models.SubtaskStateLog.objects.all()
# query by subtask
subtask = self.request.query_params.get('subtask', None)
if subtask is not None:
return queryset.filter(subtask=subtask)
return queryset
queryset = models.SubtaskStateLog.objects.all()
serializer_class = serializers.SubtaskStateLogSerializer
......@@ -4,6 +4,8 @@ This file contains the viewsets (based on the elsewhere defined data models and
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.models import User
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticatedOrReadOnly, DjangoModelPermissions
......@@ -17,6 +19,14 @@ from lofar.sas.tmss.tmss.tmssapp import serializers
from lofar.common.json_utils import get_default_json_object_for_schema
# This is required for keeping a user reference as ForeignKey in other models
# (I think so that the HyperlinkedModelSerializer can generate a URI)
class UserViewSet(ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = serializers.UserSerializer
class TagsViewSet(LOFARViewSet):
queryset = models.Tags.objects.all()
serializer_class = serializers.TagsSerializer
......@@ -135,7 +135,7 @@ router.register(r'dataproduct_archive_info', viewsets.DataproductArchiveInfoView
router.register(r'dataproduct_hash', viewsets.DataproductHashViewSet)
router.register(r'task_relation_blueprint', viewsets.TaskRelationBlueprintViewSet)
router.register(r'subtask_state_log', viewsets.SubtaskStateLogViewSet)
router.register(r'user', viewsets.UserViewSet)
# ---
......@@ -369,6 +369,31 @@ class SubtaskTestCase(unittest.TestCase):
self.assertTrue("ProtectedError" in str(response.content))
GET_and_assert_expected_response(self, specifications_template_url, 200, stt_test_data)
def test_subtask_state_log_records(self):
st_test_data = test_data_creator.Subtask()
# POST new item, verify
r_dict = POST_and_assert_expected_response(self, BASE_URL + '/subtask/', st_test_data, 201, st_test_data)
url = r_dict['url']
GET_and_assert_expected_response(self, url, 200, st_test_data)
# Verify state log count is zero
segments = url.split('/')
identifier = ''
while identifier == '':
identifier = segments.pop()
GET_and_assert_expected_response(self, BASE_URL + '/subtask_state_log/?subtask=' + identifier, 200, {"count":0})
# PATCH item with something else than state and verify no log record is created
test_patch = {"specifications_doc": {"somespec": "somevalue"}}
PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch)
GET_and_assert_expected_response(self, BASE_URL + '/subtask_state_log/?subtask=' + identifier, 200, {"count": 0})
# PATCH item with state update and verify log record is created
test_patch = {"state": BASE_URL + "/subtask_state/finishing/"}
PATCH_and_assert_expected_response(self, url, test_patch, 200, test_patch)
GET_and_assert_expected_response(self, BASE_URL + '/subtask_state_log/?subtask=' + identifier, 200, {"count": 1})
class DataproductTestCase(unittest.TestCase):
def test_dataproduct_list_apiformat(self):