diff --git a/.gitattributes b/.gitattributes index 4f4e4ad170fd7f74280945e9e41088a541d7d6ce..0149603c93452d7e707c57263ac58dcdd6a69f2f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4245,7 +4245,7 @@ SAS/LSMR/src/lsmr/lsmrapp/__init__.py -text SAS/LSMR/src/lsmr/lsmrapp/admin.py -text SAS/LSMR/src/lsmr/lsmrapp/apps.py -text SAS/LSMR/src/lsmr/lsmrapp/migrations/0001_initial.py -text -SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180704_1623.py -text +SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180705_0736.py -text SAS/LSMR/src/lsmr/lsmrapp/migrations/CMakeLists.txt -text SAS/LSMR/src/lsmr/lsmrapp/migrations/__init__.py -text SAS/LSMR/src/lsmr/lsmrapp/models.py -text diff --git a/SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180704_1623.py b/SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180705_0736.py similarity index 93% rename from SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180704_1623.py rename to SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180705_0736.py index 2c04f26e11aa074d51131e618d85604b6310dc6d..e908c2de483ed36d8c70940fc8b34ad32facc6ad 100644 --- a/SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180704_1623.py +++ b/SAS/LSMR/src/lsmr/lsmrapp/migrations/0002_auto_20180705_0736.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.6 on 2018-07-04 16:23 +# Generated by Django 2.0.6 on 2018-07-05 07:36 import django.contrib.postgres.fields import django.contrib.postgres.fields.jsonb @@ -19,7 +19,7 @@ class Migration(migrations.Migration): name='GeneratorTemplate', 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=30), blank=True, null=True, size=8)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, size=8)), ('creation', models.DateTimeField(auto_now_add=True)), ('update', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=30)), @@ -37,7 +37,7 @@ class Migration(migrations.Migration): name='RunTemplate', 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=30), blank=True, null=True, size=8)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, size=8)), ('creation', models.DateTimeField(auto_now_add=True)), ('update', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=30)), @@ -64,14 +64,14 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('role', models.CharField(choices=[('CORRELATOR', 'correlator'), ('BEAMFORMER', 'beamformer'), ('INSPECTION_PLOTS', 'inspection plots'), ('CALIBRATOR', 'calibrator'), ('TARGET', 'target'), ('INPUT_OUTPUT', 'input, output')], max_length=30)), ('datatype', models.CharField(choices=[('VISIBILITIES', 'visibilities'), ('TIME_SERIES', 'time series'), ('INSTRUMENT_MODEL', 'instrument model'), ('IMAGE', 'image'), ('QUALITY', 'quality')], max_length=30)), - ('dataformat', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('MEASUREMENTSET', 'MeasurementSet'), ('HDF5', 'HDF5')], max_length=30), blank=True, null=True, size=8)), + ('dataformat', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('MEASUREMENTSET', 'MeasurementSet'), ('HDF5', 'HDF5')], max_length=30), blank=True, size=8)), ], ), migrations.CreateModel( name='WorkRelationSelectionTemplate', 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=30), blank=True, null=True, size=8)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, size=8)), ('creation', models.DateTimeField(auto_now_add=True)), ('update', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=30)), @@ -88,7 +88,7 @@ class Migration(migrations.Migration): name='WorkRequestTemplate', 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=30), blank=True, null=True, size=8)), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=30), blank=True, size=8)), ('creation', models.DateTimeField(auto_now_add=True)), ('update', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=30)), diff --git a/SAS/LSMR/src/lsmr/lsmrapp/models.py b/SAS/LSMR/src/lsmr/lsmrapp/models.py index 58cff4769455abeb99d784e78be8629939a6bb10..a7c28a13ef263cb12eb5a7d230bc8647d473d029 100644 --- a/SAS/LSMR/src/lsmr/lsmrapp/models.py +++ b/SAS/LSMR/src/lsmr/lsmrapp/models.py @@ -2,7 +2,7 @@ This file contains the database models """ -from django.db.models import Model, CharField, DateTimeField, BooleanField, ForeignKey, CASCADE, TextField +from django.db.models import Model, CharField, DateTimeField, BooleanField, ForeignKey, CASCADE, Field from django.contrib.postgres.fields import ArrayField, JSONField from django.contrib.postgres.indexes import GinIndex from enum import Enum @@ -17,7 +17,7 @@ from enum import Enum class BasicCommon(Model): # todo: we cannot use foreign keys in the array here, so we have to keep the Tags table up to date by trigger or so. # todo: we could switch to a manytomany field instead? - tags = ArrayField(CharField(max_length=30), size=8, blank=True, null=True) + tags = ArrayField(CharField(max_length=30), size=8, blank=True) creation = DateTimeField(auto_now_add=True) update = DateTimeField(auto_now=True) @@ -47,6 +47,7 @@ class Tags(Model): title = CharField(max_length=30) description = CharField(max_length=255) + # # I/O # @@ -75,13 +76,26 @@ class DataformatChoice(Enum): HDF5 = "HDF5" +# todo: fix this! +#class EnumField(Field): # todo: Test if CharField works better e.g. for forms +# """ +# Django does not support creating db enums, so we got to do that ourselves here +# """ +# def __init__(self, *args, **kwargs): +# super(EnumField, self).__init__(*args, **kwargs) +# +# def db_type(self, connection): +# return "enum(%s)" % ','.join("'%s'" % key for (key, _) in self.choices) +# # todo: unfortunately this won't work for Postgres since a CREATE TYPE is required first... + + # concrete models class WorkIORoles(Model): - # todo: Choices does not seem to translate to a a Postgres ENUM type currently + # todo: Choices do not translate to a Postgres ENUM type currently, but are only enforced on the Django level. # todo: If we are not happy with this as is, I see two options: # todo: 1. Wait to see if they implement that EnumField in the meantime (https://code.djangoproject.com/ticket/24342) - # todo: 2. Do that in raw SQL outside Django + # todo: 2. Build this ourselves. I tried to achieve that with the EnumField above, but it won't be that easy, I guess role = CharField( max_length=30, choices=[(item.name, item.value) for item in RoleChoice] @@ -93,7 +107,7 @@ class WorkIORoles(Model): dataformat = ArrayField(CharField( max_length=30, choices=[(item.name, item.value) for item in DataformatChoice] - ), size=8, blank=True, null=True) + ), size=8, blank=True) outputs = ForeignKey("WorkRequestTemplate", related_name='role_output', on_delete=CASCADE, null=True) inputs = ForeignKey("WorkRequestTemplate", related_name='role_input', on_delete=CASCADE, null=True) diff --git a/SAS/LSMR/test/t_functional.py b/SAS/LSMR/test/t_functional.py index 0711a18f38c6ae0e2723961a659c672be4ad075c..f32aaa8e446cdef58b520233495fc57d942878ad 100755 --- a/SAS/LSMR/test/t_functional.py +++ b/SAS/LSMR/test/t_functional.py @@ -19,6 +19,16 @@ # $Id: $ + +# This functional test talks to the API like a regular user would. +# It is supposed to cover all REST http methods for all ViewSets. +# todo: For now I only covered one of the templates as they are mostly identical. I am still a bit under the impression +# todo: ...that we re-test Django functionality that we can expect to just work with some of these tests. On the other +# todo: ...hand a lot of these provide us a nice basis for differentiating out behavior in a controlled way. +# todo: So if this is generally how we want to test, we should probably still cover the other templates. +# todo: We should probably also fully test behavior wrt mandatory and nullable fields. + + import unittest import requests import json @@ -134,7 +144,7 @@ class GeneratorTemplateTestCase(unittest.TestCase): self.assertEqual(r.status_code, 200) self.assertTrue("Generator Template List" in r.content.decode('utf8')) - def test_generator_template_GET_nonexistant_item_raises_error(self): + def test_generator_template_GET_nonexistant_raises_error(self): r = requests.get(BASE_URL + '/generator_template/1234321/') self.assertEqual(r.status_code, 404) @@ -186,15 +196,160 @@ class RunTemplateTestCase(unittest.TestCase): class WorkRequestTemplateTestCase(unittest.TestCase): - # todo - pass + test_data_1 = {"name": "observation", + "description": 'My one observation', + "default_version": False, + "version": 'v0.314159265359', + "schema": {"mykey": "my value"}, + "tags": ["LSMR", "TESTING"], + "validation_code_js": "???"} + + # todo -class WorkRelationTemplateTestCase(unittest.TestCase): +class WorkRelationSelectionTemplateTestCase(unittest.TestCase): # todo pass +class WorkIORolesTestCase(unittest.TestCase): + + # test data + test_data_1 = {"role": "CALIBRATOR", + "datatype": "INSTRUMENT_MODEL", + "dataformat": ['HDF5'], + "outputs": None, + "inputs": None + } + + test_data_2 = {"role": "TARGET", + "datatype": "VISIBILITIES", + "dataformat": ['MEASUREMENTSET'], + "outputs": None, + "inputs": None + } + + test_patch = {"role": 'CORRELATOR', + "dataformat": ['MEASUREMENTSET', "HDF5"] + } + + def test_work_io_roles_list_apiformat(self): + r = requests.get(BASE_URL + '/work_io_roles/?format=api') + self.assertEqual(r.status_code, 200) + self.assertTrue("Work Io Roles List" in r.content.decode('utf8')) + + def test_work_io_roles_GET_nonexistant_raises_error(self): + r = requests.get(BASE_URL + '/work_io_roles/1234321/') + self.assertEqual(r.status_code, 404) + + def test_work_io_roles_POST_and_GET(self): + # POST and GET a new item and assert correctness + url = POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', self.test_data_1) + GET_and_assert_correct(self, url + '?format=json', self.test_data_1) + + def test_work_io_roles_POST_invalid_role_raises_error(self): + + # POST a new item with invalid choice + test_data_invalid_role = dict(self.test_data_1) + test_data_invalid_role['role'] = 'forbidden' + r = requests.post(BASE_URL + '/work_io_roles/', json=test_data_invalid_role) + self.assertEqual(r.status_code, 400) + r_json = json.loads(r.content.decode('utf-8')) + self.assertTrue('not a valid choice' in str(r_json['role'])) + + def test_work_io_roles_POST_invalid_datatype_raises_error(self): + + # POST a new item with invalid choice + test_data_invalid = dict(self.test_data_1) + test_data_invalid['datatype'] = 'forbidden' + r = requests.post(BASE_URL + '/work_io_roles/', json=test_data_invalid) + self.assertEqual(r.status_code, 400) + r_json = json.loads(r.content.decode('utf-8')) + self.assertTrue('not a valid choice' in str(r_json['datatype'])) + + def test_work_io_roles_POST_invalid_dataformat_raises_error(self): + + # POST a new item with invalid choice + test_data_invalid = dict(self.test_data_1) + test_data_invalid['dataformat'] = ['forbidden', "HDF5"] + r = requests.post(BASE_URL + '/work_io_roles/', json=test_data_invalid) + self.assertEqual(r.status_code, 400) + r_json = json.loads(r.content.decode('utf-8')) + self.assertTrue('not a valid choice' in str(r_json['dataformat'])) + + def test_work_io_roles_POST_nonexistant_inputs_raises_error(self): + + # POST a new item with wrong reference + test_data_invalid = dict(self.test_data_1) + test_data_invalid['inputs'] = BASE_URL + "/work_request_template/6353748/" + r = requests.post(BASE_URL + '/work_io_roles/', json=test_data_invalid) + self.assertEqual(r.status_code, 400) + r_json = json.loads(r.content.decode('utf-8')) + self.assertTrue('Invalid hyperlink' in str(r_json['inputs'])) + + def test_work_io_roles_POST_existing_inputs_works(self): + + # First POST a new item to reference + url = POST_and_assert_correct(self, BASE_URL + '/work_request_template/', WorkRequestTemplateTestCase.test_data_1) + + # POST a new item with correct reference + test_data_valid = dict(self.test_data_1) + test_data_valid['inputs'] = url + POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', test_data_valid) + + def test_work_io_roles_POST_nonexistant_outputs_raises_error(self): + + # POST a new item with wrong reference + test_data_invalid = dict(self.test_data_1) + test_data_invalid['outputs'] = BASE_URL + "/work_request_template/6353748/" + r = requests.post(BASE_URL + '/work_io_roles/', json=test_data_invalid) + self.assertEqual(r.status_code, 400) + r_json = json.loads(r.content.decode('utf-8')) + self.assertTrue('Invalid hyperlink' in str(r_json['outputs'])) + + def test_work_io_roles_POST_existing_outputs_works(self): + + # First POST a new item to reference + url = POST_and_assert_correct(self, BASE_URL + '/work_request_template/', WorkRequestTemplateTestCase.test_data_1) + + # POST a new item with correct reference + test_data_valid = dict(self.test_data_1) + test_data_valid['outputs'] = url + POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', test_data_valid) + + def test_work_io_roles_PUT_nonexistant_raises_error(self): + r = requests.put(BASE_URL + '/work_io_roles/9876789876/', self.test_data_1) + self.assertEqual(r.status_code, 404) + + def test_work_io_roles_PUT(self): + # POST new item, verify + url = POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', self.test_data_1) + GET_and_assert_correct(self, url, self.test_data_1) + + # PUT new values, verify + PUT_and_assert_correct(self, url, self.test_data_2) + GET_and_assert_correct(self, url, self.test_data_2) + + def test_work_io_roles_PATCH(self): + # POST new item, verify + url = POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', self.test_data_1) + GET_and_assert_correct(self, url, self.test_data_1) + + # PATCH item and verify + PATCH_and_assert_correct(self, url, self.test_patch) + expected_data = dict(self.test_data_1) + expected_data.update(self.test_patch) + GET_and_assert_correct(self, url, expected_data) + + def test_work_io_roles_DELETE(self): + # POST new item, verify + url = POST_and_assert_correct(self, BASE_URL + '/work_io_roles/', self.test_data_1) + GET_and_assert_correct(self, url, self.test_data_1) + + # DELETE and check it's gone + DELETE_and_assert_gone(self, url) + + if __name__ == "__main__": unittest.main() diff --git a/SAS/LSMR/test/t_lsmrapp_models.py b/SAS/LSMR/test/t_lsmrapp_models.py index f90acd53fb36631b1abd7e7067b4e9c7e9cd5f7e..c906eecf1153e20fefc0f48290b4229377731dce 100755 --- a/SAS/LSMR/test/t_lsmrapp_models.py +++ b/SAS/LSMR/test/t_lsmrapp_models.py @@ -19,6 +19,7 @@ # $Id: $ +import unittest import rest_framework.test from lsmr.lsmrapp import models @@ -360,16 +361,16 @@ class WorkIORolesTest(rest_framework.test.APITransactionTestCase): reset_sequences = True # test data - test_data_1 = {"role": "", - "datatype": "", - "dataformat": [], + test_data_1 = {"role": "CALIBRATOR", + "datatype": "INSTRUMENT_MODEL", + "dataformat": ['HDF5'], "outputs": None, "inputs": None, } - test_data_2 = {"role": "", - "datatype": "", - "dataformat": [], + test_data_2 = {"role": "TARGET", + "datatype": "VISIBILITIES", + "dataformat": ['MEASUREMENTSET'], "outputs": None, "inputs": None, } @@ -405,5 +406,39 @@ class WorkIORolesTest(rest_framework.test.APITransactionTestCase): for item in self.test_data_2.items(): self.assertIn(item, response2.data.items()) + @unittest.skip("Database does not enforce choices currently") + def test_WorkIORoles_prevents_invalid_role(self): + # setup + test_data_invalidrole = dict(self.test_data_1) + test_data_invalidrole['role'] = 'forbidden' + + with self.assertRaises(TypeError): + models.WorkIORoles.objects.create(**test_data_invalidrole) + + + @unittest.skip("Database does not enforce choices currently") + def test_WorkIORoles_prevents_invalid_datatype(self): + # setup + test_data_invalidrole = dict(self.test_data_1) + test_data_invalidrole['datatype'] = 'forbidden' + + with self.assertRaises(TypeError): + models.WorkIORoles.objects.create(**test_data_invalidrole) + + @unittest.skip("Database does not enforce choices currently") + def test_WorkIORoles_prevents_invalid_dataformat(self): + # setup + test_data_invalidrole = dict(self.test_data_1) + test_data_invalidrole['dataformat'] = ['HDF5', 'forbidden'] + + with self.assertRaises(TypeError): + models.WorkIORoles.objects.create(**test_data_invalidrole) + + + # todo: def test_WorkIORoles_prevents_missing_inputs(self): + + # todo: def test_WorkIORoles_prevents_missing_outputs(self): + + # todo: def test_WorkIORoles_allows_existing_inputs_and_outputs(self): # todo: Tags? -> Decide how to deal with them first.