diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0007_systemevent.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0007_systemevent.py new file mode 100644 index 0000000000000000000000000000000000000000..0c9244dae2cdb26ba25dada3ed13c93facafe68e --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0007_systemevent.py @@ -0,0 +1,127 @@ +# Generated by Django 3.0.9 on 2021-10-18 14:36 + +from django.conf import settings +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0006_subtask_obsolete_since'), + ] + + operations = [ + migrations.CreateModel( + name='SystemEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), + ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)), + ('description', models.CharField(blank=True, default='', help_text='A longer description of this object.', max_length=255)), + ('start', models.DateTimeField(help_text='When this event started.')), + ('stop', models.DateTimeField(help_text='When this event stopped (NULLable).', null=True)), + ('notes', models.CharField(help_text='Any additional information', max_length=255, null=True)), + ('affected_hardware_doc', django.contrib.postgres.fields.jsonb.JSONField(help_text='Properties of this event, e.g. station list and other hardware affected')), + ('jira_url', models.CharField(help_text='Link to JIRA issue (if any)', max_length=255, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemEventSeverity', + fields=[ + ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemEventStatus', + fields=[ + ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemEventSubtype', + fields=[ + ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemEventTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), + ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)), + ('description', models.CharField(blank=True, default='', help_text='A longer description of this object.', max_length=255)), + ('version', models.IntegerField(editable=False, help_text='Version of this template (with respect to other templates of the same name)')), + ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemEventType', + fields=[ + ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddConstraint( + model_name='systemeventtemplate', + constraint=models.UniqueConstraint(fields=('name', 'version'), name='systemeventtemplate_unique_name_version'), + ), + migrations.AddField( + model_name='systemevent', + name='affected_hardware_template', + field=models.ForeignKey(help_text='Template for the affected_hardware_doc.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SystemEventTemplate'), + ), + migrations.AddField( + model_name='systemevent', + name='affected_tasks', + field=models.ManyToManyField(blank=True, help_text='The task blueprints that are affected by this issue', related_name='system_events', to='tmssapp.TaskBlueprint'), + ), + migrations.AddField( + model_name='systemevent', + name='created_by', + field=models.ForeignKey(help_text='User who created this entry.', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='systemevent', + name='issue_subtype', + field=models.ForeignKey(help_text='The subtype that classifies this issue', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SystemEventSubtype'), + ), + migrations.AddField( + model_name='systemevent', + name='issue_type', + field=models.ForeignKey(help_text='The main type that classifies this issue', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SystemEventType'), + ), + migrations.AddField( + model_name='systemevent', + name='severity', + field=models.ForeignKey(help_text='The subtype that classifies this issue', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SystemEventSeverity'), + ), + migrations.AddField( + model_name='systemevent', + name='status', + field=models.ForeignKey(help_text='The current status of this issue', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SystemEventStatus'), + ), + ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0008_populate_systemevent.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0008_populate_systemevent.py new file mode 100644 index 0000000000000000000000000000000000000000..276e0dada2da5f7f6aac8254fd429caf563c5f60 --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0008_populate_systemevent.py @@ -0,0 +1,14 @@ + + +from django.db import migrations + +from lofar.sas.tmss.tmss.tmssapp.populate import * + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0007_systemevent'), + ] + + operations = [migrations.RunPython(populate_system_event_choices)] + diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index f5d7828b5d11ad14b4657746b4d458bb7a489bf6..54699653af62a4361cd9fc90020bf7166ef01851 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -25,6 +25,7 @@ from lofar.sas.tmss.tmss.exceptions import BlueprintCreationException, TMSSExcep from django.db.models import Count from copy import deepcopy from lofar.common.util import subdict_of_pointer_items +from .permissions import TMSSUser as User # # I/O @@ -164,6 +165,52 @@ class ProjectState(AbstractChoice): SUSPENDED = "suspended" +class SystemEventType(AbstractChoice): + """Defines the model and predefined list of possible issue_types of a SystemEvent. + The items in the Choices class below are automagically populated into the database via a data migration.""" + class Choices(Enum): + CEP = "cep" + COBALT = "cobalt" + ENVIRONMENT = "environment" + HUMAN ="human" + NETWORK = "network" + STATION = "station" + SYSTEM = "system" + OTHER = "other" + + +class SystemEventSubtype(AbstractChoice): + """Defines the model and predefined list of possible issue_subtypes of a SystemEvent. + The items in the Choices class below are automagically populated into the database via a data migration.""" + class Choices(Enum): + CRASH = "crash" + DATALOSS = "dataloss" + HARDWARE = "hardware" + NOISY = "noisy" + OSCILLATING = "oscillating" + RFI = "rfi" + SETUP = "setup" + TEMPERATURE = "temperature" + OTHER = "other" + + +class SystemEventSeverity(AbstractChoice): + """Defines the model and predefined list of possible severity levels of a SystemEvent. + The items in the Choices class below are automagically populated into the database via a data migration.""" + class Choices(Enum): + FAILURE = "failure" + MINOR = "minor" + MAJOR = "major" + + +class SystemEventStatus(AbstractChoice): + """Defines the model and predefined list of possible statuses of a SystemEvent. + The items in the Choices class below are automagically populated into the database via a data migration.""" + class Choices(Enum): + OPEN = "open" + ANALYSED = "analysed" + + # concrete models class Setting(BasicCommon): @@ -368,6 +415,9 @@ class ReservationStrategyTemplate(BaseStrategyTemplate): class ReservationTemplate(Template): pass +class SystemEventTemplate(Template): + pass + # # Instance Objects @@ -1016,6 +1066,13 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo return fields_found + @property + def system_events(self): + events = [] + for task in self.task_blueprints.all(): + events += task.system_events + return set(events) + class TaskDraft(NamedCommon, TemplateSchemaMixin, ProjectPropertyMixin): short_description = CharField(max_length=32, help_text='A short description of this task, usually the name of the target and abbreviated task type.', blank=True, default="") @@ -1438,3 +1495,17 @@ class Reservation(NamedCommon, TemplateSchemaMixin): self.annotate_validate_add_defaults_to_doc_using_template('specifications_doc', 'specifications_template') super().save(force_insert, force_update, using, update_fields) + +class SystemEvent(NamedCommon): + created_by = ForeignKey(User, null=False, on_delete=PROTECT, help_text='User who created this entry.') + start = DateTimeField(help_text='When this event started.') + stop = DateTimeField(null=True, help_text='When this event stopped (NULLable).') + issue_type = ForeignKey('SystemEventType', on_delete=PROTECT, help_text='The main type that classifies this issue') + issue_subtype = ForeignKey('SystemEventSubtype', on_delete=PROTECT, help_text='The subtype that classifies this issue') + severity = ForeignKey('SystemEventSeverity', on_delete=PROTECT, help_text='The subtype that classifies this issue') + notes = CharField(max_length=255, null=True, help_text='Any additional information') + status = ForeignKey('SystemEventStatus', on_delete=PROTECT, help_text='The current status of this issue') + affected_hardware_doc = JSONField(help_text='Properties of this event, e.g. station list and other hardware affected') + affected_hardware_template = ForeignKey('SystemEventTemplate', on_delete=PROTECT, help_text='Template for the affected_hardware_doc.') + affected_tasks = ManyToManyField('TaskBlueprint', blank=True, related_name='system_events', help_text='The task blueprints that are affected by this issue') + jira_url = CharField(max_length=255, null=True, help_text='Link to JIRA issue (if any)') \ No newline at end of file diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index e7c757539af412c484ff799f7bb48ad9fe8b4056..629626766cc90f5e21e9398633b7bcc4b47ebf2c 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -134,6 +134,19 @@ def populate_choices(apps, schema_editor): executor.map(lambda choice_class: choice_class.objects.bulk_create([choice_class(value=x.value) for x in choice_class.Choices]), choice_classes) +def populate_system_event_choices(apps, schema_editor): + ''' + populate each 'choice' table in the database with the 'static' list of 'choice'.Choices for + each SystemEvent-related 'choice'type that has been added in a later migration than the initial populate. + :return: None + ''' + choice_classes = [SystemEventType, SystemEventSubtype, SystemEventSeverity, SystemEventStatus] + + # upload choices in parallel + with ThreadPoolExecutor() as executor: + executor.map(lambda choice_class: choice_class.objects.bulk_create([choice_class(value=x.value) for x in choice_class.Choices]), + choice_classes) + def populate_subtask_allowed_state_transitions(apps, schema_editor): '''populate the SubtaskAllowedStateTransitions table with the allowed state transitions as defined by the design in https://support.astron.nl/confluence/display/TMSS/Subtask+State+Machine''' DEFINING = SubtaskState.objects.get(value=SubtaskState.Choices.DEFINING.value) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/common_schema_template/affectedhardware-1.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/system_event_template/affectedhardware-1.json similarity index 90% rename from SAS/TMSS/backend/src/tmss/tmssapp/schemas/common_schema_template/affectedhardware-1.json rename to SAS/TMSS/backend/src/tmss/tmssapp/schemas/system_event_template/affectedhardware-1.json index c616feeecca18defca10051e11bc36c7406a31ad..8d89e603e422b37e8920d7beb0d92f3383632481 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/common_schema_template/affectedhardware-1.json +++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/system_event_template/affectedhardware-1.json @@ -1,5 +1,5 @@ { - "$id": "http://127.0.0.1:8000/api/schemas/commonschematemplate/affectedhardware/1#", + "$id": "http://127.0.0.1:8000/api/schemas/systemeventtemplate/affectedhardware/1#", "$schema": "http://json-schema.org/draft-06/schema#", "description": "This schema defines the hardware that was affected by a system event.", "properties": { diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py index d17bf85c214e9cf38e3167fd278e9ddc415d966e..4967b7fca4bdbbe5794f827f5d3d784ea16fd91d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py @@ -404,3 +404,47 @@ class ProjectStateSerializer(DynamicRelationalHyperlinkedModelSerializer): class Meta: model = models.ProjectState fields = '__all__' + + +class SystemEventTypeSerializer(DynamicRelationalHyperlinkedModelSerializer): + + class Meta: + model = models.SystemEventType + fields = '__all__' + + +class SystemEventSubtypeSerializer(DynamicRelationalHyperlinkedModelSerializer): + + class Meta: + model = models.SystemEventSubtype + fields = '__all__' + + +class SystemEventSeveritySerializer(DynamicRelationalHyperlinkedModelSerializer): + + class Meta: + model = models.SystemEventSeverity + fields = '__all__' + + +class SystemEventStatusSerializer(DynamicRelationalHyperlinkedModelSerializer): + + class Meta: + model = models.SystemEventStatus + fields = '__all__' + + +class SystemEventTemplateSerializer(AbstractTemplateSerializer): + + class Meta: + model = models.SystemEventTemplate + fields = '__all__' + + +class SystemEventSerializer(DynamicRelationalHyperlinkedModelSerializer): + affected_hardware_doc = JSONEditorField(schema_source='affected_hardware_template.schema') + created_by = UserSerializer(required=False) + + class Meta: + model = models.SystemEvent + fields = '__all__' \ No newline at end of file diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index 75935d1ee3134c23085a5cf9baabaf2d342e773e..fe9046e3761e395f3226d1afae9da2936d485845 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -584,11 +584,36 @@ class ProjectCategoryViewSet(LOFARViewSet): class NumberInFilter(filters.BaseInFilter, filters.NumberFilter): """ - Custom filter for comma-separated lists of numbers + Custom filter that accepts a comma-separated lists of numbers. + Returns objects where the (single) numeric value of the filtered field is in the provided list. """ pass +class CharInFilter(filters.BaseInFilter, filters.CharFilter): + """ + Custom filter that accepts a comma-separated lists of numbers. + Filters for objects where the (single) numeric value of the filtered field is in the provided list. + """ + pass + + +class ListContainsAnyFilter(filters.BaseCSVFilter): + """ + Custom filter for where both the filtered field and the filter input are comma-separated lists. + Filters for objects where any of the values in the provided list is contained in the filtered field. + """ + def filter(self, qs, values): + from django.db.models import Q + query = Q() + if not values: + return qs + else: + for value in values: + query |= Q(**{self.field_name+'__contains': value.strip()}) + return qs.filter(query) + + class FloatRangeField(filter_fields.RangeField): """ Custom range field for float values. The normal django filter_fields.RangeField uses (integer) decimal fields. @@ -1009,7 +1034,15 @@ class SchedulingUnitBlueprintViewSet(LOFARViewSet): return Response(serializers.SchedulingUnitDraftExtendedSerializer(scheduling_unit_draft_copy, context={'request':request}).data, status=status.HTTP_201_CREATED) - + @swagger_auto_schema(responses={200: 'The system events of this scheduling unit blueprint', + 403: 'forbidden'}, + operation_description="Get the system events of this scheduling unit blueprint.") + @action(methods=['get'], detail=True, url_name="system_events") + def system_events(self, request, pk=None): + sub = get_object_or_404(models.SchedulingUnitBlueprint, pk=pk) + system_events = self.filter_queryset(sub.system_events) + serializer = self.get_serializer(system_events, many=True) + return RestResponse(serializer.data) class SchedulingUnitBlueprintExtendedViewSet(SchedulingUnitBlueprintViewSet): @@ -1311,6 +1344,17 @@ class TaskBlueprintViewSet(LOFARViewSet): return RestResponse(serializer.data) + @swagger_auto_schema(responses={200: 'The system events of this task blueprint', + 403: 'forbidden'}, + operation_description="Get the system events of this task blueprint.") + @action(methods=['get'], detail=True, url_name="system_events") + def system_events(self, request, pk=None): + tb = get_object_or_404(models.TaskBlueprint, pk=pk) + system_events = self.filter_queryset(tb.system_events) + serializer = self.get_serializer(system_events, many=True) + return RestResponse(serializer.data) + + class TaskBlueprintNestedViewSet(LOFARNestedViewSet): queryset = models.TaskBlueprint.objects.all() serializer_class = serializers.TaskBlueprintSerializer @@ -1384,3 +1428,64 @@ class PriorityQueueTypeViewSet(LOFARViewSet): class ProjectStateViewSet(LOFARViewSet): queryset = models.ProjectState.objects.all() serializer_class = serializers.ProjectStateSerializer + + +class SystemEventTypeViewSet(LOFARViewSet): + queryset = models.SystemEventType.objects.all() + serializer_class = serializers.SystemEventTypeSerializer + + +class SystemEventSubtypeViewSet(LOFARViewSet): + queryset = models.SystemEventSubtype.objects.all() + serializer_class = serializers.SystemEventSubtypeSerializer + + +class SystemEventSeverityViewSet(LOFARViewSet): + queryset = models.SystemEventSeverity.objects.all() + serializer_class = serializers.SystemEventSeveritySerializer + + +class SystemEventStatusViewSet(LOFARViewSet): + queryset = models.SystemEventStatus.objects.all() + serializer_class = serializers.SystemEventStatusSerializer + + +class SystemEventTemplateViewSet(AbstractTemplateViewSet): + queryset = models.SystemEventTemplate.objects.all() + serializer_class = serializers.SystemEventTemplateSerializer + + +class SystemEventFilter(filters.FilterSet): + name = filters.CharFilter(field_name='name', lookup_expr='icontains') + created_by = filters.CharFilter(field_name='created_by__username', lookup_expr='icontains') + 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') + created_at = filters.IsoDateTimeFromToRangeFilter(field_name='created_at') + updated_at = filters.IsoDateTimeFromToRangeFilter(field_name='updated_at') + description = filters.CharFilter(field_name='description', lookup_expr='icontains') + start = filters.IsoDateTimeFromToRangeFilter(field_name='start') + stop = filters.IsoDateTimeFromToRangeFilter(field_name='stop') + issue_type = filters.ModelMultipleChoiceFilter(field_name='issue_type', queryset=models.SystemEventType.objects.all()) + issue_subtype = filters.ModelMultipleChoiceFilter(field_name='issue_subtype', queryset=models.SystemEventSubtype.objects.all()) + severity = filters.ModelMultipleChoiceFilter(field_name='severity', queryset=models.SystemEventSeverity.objects.all()) + notes = filters.CharFilter(field_name='notes', lookup_expr='icontains') + status = filters.ModelMultipleChoiceFilter(field_name='status', queryset=models.SystemEventStatus.objects.all()) + affected_hardware_doc_stations = ListContainsAnyFilter(field_name='affected_hardware_doc__stations') + affected_hardware_template_name = filters.CharFilter(field_name='affected_hardware_template__name', lookup_expr='icontains') + affected_tasks = filters.ModelMultipleChoiceFilter(field_name='affected_tasks', queryset=models.TaskBlueprint.objects.all()) + jira_url = filters.CharFilter(field_name='jira_url', lookup_expr='icontains') + + class Meta: + model = models.SystemEvent + fields = '__all__' + filter_overrides = FILTER_OVERRIDES + + +class SystemEventViewSet(LOFARViewSet): + queryset = models.SystemEvent.objects.all() + serializer_class = serializers.SystemEventSerializer + filter_class = SystemEventFilter + + def perform_create(self, serializer): + serializer.save(created_by=self.request.user) diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index fcd6f55b39c3ee0c26090ae2a5465b4610176f18..790db62e4ade2fcf3018ccd22d489144a6e89fca 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -133,6 +133,10 @@ router.register(r'quantity', viewsets.QuantityViewSet) router.register(r'task_type', viewsets.TaskTypeViewSet) router.register(r'priority_queue_type', viewsets.PriorityQueueTypeViewSet) router.register(r'project_state', viewsets.ProjectStateViewSet) +router.register(r'system_event_status', viewsets.SystemEventStatusViewSet) +router.register(r'system_event_type', viewsets.SystemEventTypeViewSet) +router.register(r'system_event_subtype', viewsets.SystemEventSubtypeViewSet) +router.register(r'system_event_severity', viewsets.SystemEventSeverityViewSet) # templates router.register(r'common_schema_template', viewsets.CommonSchemaTemplateViewSet) @@ -146,6 +150,7 @@ router.register(r'task_relation_selection_template', viewsets.TaskRelationSelect router.register(r'reservation_template', viewsets.ReservationTemplateViewSet) router.register(r'task_connector_type', viewsets.TaskConnectorTypeViewSet) router.register(r'reservation_strategy_template', viewsets.ReservationStrategyTemplateViewSet) +router.register(r'system_event_template', viewsets.SystemEventTemplateViewSet) # instances router.register(r'cycle', viewsets.CycleViewSet) @@ -156,6 +161,7 @@ router.register(r'project_quota', viewsets.ProjectQuotaViewSet) router.register(r'project_quota_archive_location', viewsets.ProjectQuotaArchiveLocationViewSet) router.register(r'setting', viewsets.SettingViewSet) router.register(r'reservation', viewsets.ReservationViewSet) +router.register(r'system_event', viewsets.SystemEventViewSet) router.register(r'scheduling_set', viewsets.SchedulingSetViewSet) router.register(r'scheduling_unit_draft_extended', viewsets.SchedulingUnitDraftExtendedViewSet) 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 ee4e0ceac36ff5acb8100c810e5d76dbfcb765f3..6ca9620e536d7e238c86961a9fe77464050eabc8 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py @@ -3472,6 +3472,51 @@ class SystemRolePermissionTestCase(unittest.TestCase): self.assertEqual(response.status_code, 403) self.assertEqual(count, len(models.Project.objects.all())) +class SystemEventTestCase(unittest.TestCase): + + def test_SystemEvent_list_apiformat(self): + r = requests.get(BASE_URL + '/system_event/?format=api', auth=AUTH) + self.assertEqual(r.status_code, 200) + self.assertTrue("System Event List" in r.content.decode('utf8')) + + def test_SystemEvent_POST_and_GET(self): + se_test_data = test_data_creator.SystemEvent("se_%s" % uuid.uuid4()) + + # POST and GET a new item and assert correctness + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/system_event/', se_test_data, 201, se_test_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, se_test_data) + + + def test_SystemEvent_filters_for_stations(self): + template_url = GET_and_assert_equal_expected_code(self, BASE_URL + '/system_event_template/?name=affectedhardware', 200)['results'][0]['url'] + + se_test_data_1 = test_data_creator.SystemEvent("se_%s" % uuid.uuid4(), affected_hardware_template=template_url, + affected_hardware_doc={'stations': ['DE601', 'DE602']}) + se_test_data_2 = test_data_creator.SystemEvent("se_%s" % uuid.uuid4(), affected_hardware_template=template_url, + affected_hardware_doc={'stations': ['DE601', 'DE603']}) + se_test_data_3 = test_data_creator.SystemEvent("se_%s" % uuid.uuid4(), affected_hardware_template=template_url, + affected_hardware_doc={'stations': ['DE604', 'DE609']}) + + POST_and_assert_expected_response(self, BASE_URL + '/system_event/', se_test_data_1, 201, se_test_data_1) + POST_and_assert_expected_response(self, BASE_URL + '/system_event/', se_test_data_2, 201, se_test_data_2) + POST_and_assert_expected_response(self, BASE_URL + '/system_event/', se_test_data_3, 201, se_test_data_3) + + # assert + response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/system_event/?affected_hardware_doc_stations=%s' % 'DE601', 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/system_event/?affected_hardware_doc_stations=%s' % 'DE603,DE604', 200) + response_3 = GET_and_assert_equal_expected_code(self, BASE_URL + '/system_event/?affected_hardware_doc_stations=%s' % 'CS999', 200) + + self.assertEqual(response_1['count'], 2) + self.assertIn(se_test_data_1['name'], str(response_1)) + self.assertIn(se_test_data_2['name'], str(response_1)) + self.assertNotIn(se_test_data_3['name'], str(response_1)) + self.assertEqual(response_2['count'], 2) + self.assertNotIn(se_test_data_1['name'], str(response_2)) + self.assertIn(se_test_data_2['name'], str(response_2)) + self.assertIn(se_test_data_3['name'], str(response_2)) + self.assertEqual(response_3['count'], 0) + if __name__ == "__main__": unittest.main() diff --git a/SAS/TMSS/backend/test/tmss_test_data_rest.py b/SAS/TMSS/backend/test/tmss_test_data_rest.py index 7cdbbe22373511c76859dbbfc3b8b36c0989e22f..71bf02695ce6f4256cc44ab2f4435fe76a743020 100644 --- a/SAS/TMSS/backend/test/tmss_test_data_rest.py +++ b/SAS/TMSS/backend/test/tmss_test_data_rest.py @@ -866,6 +866,44 @@ class TMSSRESTTestDataCreator(): 'DELETE': DELETE or [], 'POST': POST or []} + + def SystemEventTemplate(self, name="systemeventtemplate1", schema:dict=None) -> dict: + if schema is None: + schema = minimal_json_schema(properties={"foo": {"type": "string", "default": "bar"}}) + + return { "name": name, + "description": 'My description', + "schema": schema, + "tags": ["TMSS", "TESTING"]} + + @property + def cached_system_event_template_url(self): + try: + return self._system_event_template_url + except AttributeError: + self._system_event_template_url = self.post_data_and_get_url(self.SystemEventTemplate(), '/system_event_template/') + return self._system_event_template_url + + + def SystemEvent(self, name="My System Event", start=None, stop=None, issue_type="human", issue_subtype="dataloss", + severity="major", status="open", affected_hardware_doc=None, affected_hardware_template=None): + + if affected_hardware_doc is None: + affected_hardware_doc = {} + + if affected_hardware_template is None: + affected_hardware_template = self.cached_system_event_template_url + + return {"affected_hardware_doc": affected_hardware_doc, + "affected_hardware_template": affected_hardware_template, + "issue_subtype": self.django_api_url + '/system_event_subtype/%s' % issue_subtype, + "issue_type": self.django_api_url + '/system_event_type/%s' % issue_type, + "name": name, + "severity": self.django_api_url + '/system_event_severity/%s' % severity, + "start": start if start is not None else datetime.utcnow().isoformat(), + "stop": stop if stop is not None else datetime.utcnow().isoformat(), + "status": self.django_api_url + '/system_event_status/%s' % status} + def wipe_cache(self): cached_url_attributes = [attr for attr in self.__dict__.keys() if attr.startswith('_') and attr.endswith('_url')] for attr in cached_url_attributes: