diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ad874e2280585a04f9213b366fe4c7063047c1d..4d12b0f61fe0151b0dc2cc12f2a9383d37a81012 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,25 @@ stages: + - test - build - deploy_to_test - deploy_to_production +docker-test: + image: python:3.6.7-alpine + stage: test + services: + - postgres:11.0 + variables: + POSTGRES_DB: ldv-spec-db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: "atdb123" + script: + - cd ldvspec + - pip install -r requirements/dev.txt + - python manage.py makemigrations + - python manage.py migrate + - python manage.py test + docker-build-master: # Official docker image. image: docker:latest diff --git a/ldvspec/ldvspec/settings/base.py b/ldvspec/ldvspec/settings/base.py index 6734ff05c266eee3d4adfbcc3d9ffb8068683abd..24b8ce6551cf34df22f3ac1bed4f7c0485cbf939 100644 --- a/ldvspec/ldvspec/settings/base.py +++ b/ldvspec/ldvspec/settings/base.py @@ -131,7 +131,4 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 100 } -try: - ATDB_HOST = os.environ['ATDB_HOST'] -except: - ATDB_HOST = "http://localhost:8000/atdb/" \ No newline at end of file +ATDB_HOST = os.environ.get('ATDB_HOST', 'http://localhost:8000/atdb/') diff --git a/ldvspec/ldvspec/settings/ci.py b/ldvspec/ldvspec/settings/ci.py new file mode 100644 index 0000000000000000000000000000000000000000..ead5103b7942225b19cefb40ee9300dbf27074da --- /dev/null +++ b/ldvspec/ldvspec/settings/ci.py @@ -0,0 +1,24 @@ +from ldvspec.settings.base import * + +# SECURITY WARNING: don't run with debug turned on in production! +DEV = True +DEBUG = True + +ALLOWED_HOSTS = ["*"] +CORS_ORIGIN_ALLOW_ALL = True + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'USER': 'postgres', + 'PASSWORD': 'atdb123', + 'NAME': 'ldv-spec-db', + 'HOST': 'postgres', + 'PORT': '5432', + }, +} + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [] diff --git a/ldvspec/lofardata/models.py b/ldvspec/lofardata/models.py index 0d233fc9c6f7c4d54490f66bcda851750b9c6459..882475e59bc4c1f53fe7916b2b76c7630bbb4f75 100644 --- a/ldvspec/lofardata/models.py +++ b/ldvspec/lofardata/models.py @@ -1,13 +1,27 @@ from django.db import models from django_filters import rest_framework as filters +from urllib.parse import urlsplit - - -class DataLocations(models.Model): +class DataLocation(models.Model): name = models.CharField(max_length=50, primary_key=True) uri = models.CharField(max_length=200) + @staticmethod + def insert_location_from_string(location_string): + """ + Insert a datalocation from a srm string (e.g. srm://surm:4321/path.tar) + + :param str location_string: SRM url + :return: DataLocation object + rtype: DataLocations + """ + _, netloc, *_ = urlsplit(location_string) + + dataloc = DataLocation(name=netloc, uri=location_string.rstrip('/')) + dataloc.save() + return dataloc + class DataProduct(models.Model): obs_id = models.CharField(verbose_name="OBS_ID", max_length=15) @@ -15,16 +29,40 @@ class DataProduct(models.Model): dataproduct_source = models.CharField(max_length=20) dataproduct_type = models.CharField(max_length=50) project = models.CharField(max_length=15) - location = models.ForeignKey(DataLocations, on_delete=models.DO_NOTHING) + location = models.ForeignKey(DataLocation, on_delete=models.DO_NOTHING) activity = models.CharField(max_length=50) surl = models.CharField(max_length=200) filesize = models.PositiveBigIntegerField() additional_meta = models.JSONField() + @staticmethod + def insert_dataproduct(obs_id, + oid_source, + dataproduct_source, + dataproduct_type, + project, + activity, + surl, + filesize, + additional_meta): + scheme, netloc, *_ = urlsplit(surl) + + dp = DataProduct(obs_id=obs_id, + oid_source=oid_source, + dataproduct_source=dataproduct_source, + dataproduct_type=dataproduct_type, + project=project, + location=DataLocation.insert_location_from_string('://'.join((scheme, netloc))), + activity=activity, + filesize=filesize, + additional_meta=additional_meta, + surl=surl + ) + dp.save() + return dp -class DataProductFilter(models.Model): +class DataProductFilter(models.Model): field = models.CharField(max_length=20) name = models.CharField(max_length=20) lookup_type = models.CharField(max_length=100) - diff --git a/ldvspec/lofardata/serializers.py b/ldvspec/lofardata/serializers.py index 189e327c18d9210cd26b23ba53bd65f3051458b1..f685fb619d7b66f46ef00023be19a953accd413a 100644 --- a/ldvspec/lofardata/serializers.py +++ b/ldvspec/lofardata/serializers.py @@ -1,8 +1,25 @@ +from abc import ABC + from rest_framework import serializers -from .models import DataProduct +from .models import DataProduct, DataLocation + + +class DataLocationSerializer(serializers.ModelSerializer): + class Meta: + model = DataLocation + fields = '__all__' -class DataProductSerializer(serializers.ModelSerializer): +class DataProductSerializer(serializers.ModelSerializer): class Meta: model = DataProduct fields = "__all__" + + +class DataProductFlatSerializer(serializers.ModelSerializer): + class Meta: + model = DataProduct + exclude = ('location',) + + def create(self, validated_data): + return DataProduct.insert_dataproduct(**validated_data) diff --git a/ldvspec/lofardata/tests.py b/ldvspec/lofardata/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/ldvspec/lofardata/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/ldvspec/__init__.py b/ldvspec/lofardata/tests/__init__.py similarity index 100% rename from ldvspec/__init__.py rename to ldvspec/lofardata/tests/__init__.py diff --git a/ldvspec/lofardata/tests/test_data_filter.py b/ldvspec/lofardata/tests/test_data_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..ba051c47621f23e89dd292a7ebcbaa9881045885 --- /dev/null +++ b/ldvspec/lofardata/tests/test_data_filter.py @@ -0,0 +1,56 @@ +import rest_framework.test as rtest +from django.contrib.auth.models import User +from lofardata.models import DataProduct, DataProductFilter + +test_object_values = [dict(obs_id='12345', oid_source='SAS', dataproduct_source='lofar', + dataproduct_type='observation', + project='LT10_10', + activity='observation', + surl='srm://surfsara.nl:4884/...', + filesize=40, + additional_meta={'dysco_compression': True}), + dict(obs_id='12346', oid_source='SAS', dataproduct_source='lofar', + dataproduct_type='observation', + project='LT10_10', + activity='pipeline', + surl='srm://surfsara.nl:4884/...', + filesize=40, + additional_meta={'dysco_compression': True}), + dict(obs_id='12347', oid_source='SAS', dataproduct_source='lofar', + dataproduct_type='observation', + project='LT10_10', + activity='observation', + surl='srm://surfsara.nl:4884/...', + filesize=40, + additional_meta={'dysco_compression': True})] + + +class TestDataFilter(rtest.APITestCase): + def setUp(self): + self.client = rtest.APIClient() + self.user = User.objects.create_superuser('admin') + self.client.force_authenticate(self.user) + + for test_object in test_object_values: + DataProduct.insert_dataproduct(**test_object) + + def get_current_query_parameters(self): + response = self.client.get('/ldvspec/openapi/') + query_parameters = response.data['paths']['/ldvspec/data/']['get']['parameters'] + return {parameter['name']: parameter for parameter in query_parameters} + + def test_add_custom_filter(self): + self.assertNotIn('activity', self.get_current_query_parameters(), 'Test is invalid! Update') + + dp_filter = DataProductFilter(field='activity', name='activity_type', lookup_type='exact') + dp_filter.save() + + response = self.client.get('/ldvspec/data/?activity_type=pipeline') + self.assertEqual(1, response.data['count']) + self.assertEqual('pipeline', response.data['results'][0]['activity']) + + response = self.client.get('/ldvspec/data/?activity_type=observation') + self.assertEqual(2, response.data['count']) + self.assertEqual('observation', response.data['results'][0]['activity']) + + self.assertIn('activity_type', self.get_current_query_parameters()) \ No newline at end of file diff --git a/ldvspec/lofardata/tests/test_dataproduct.py b/ldvspec/lofardata/tests/test_dataproduct.py new file mode 100644 index 0000000000000000000000000000000000000000..7d63d9f5c4415f3ca275f0e83132c2671bb98b9c --- /dev/null +++ b/ldvspec/lofardata/tests/test_dataproduct.py @@ -0,0 +1,80 @@ +import django.test as dtest +import rest_framework.test as rtest +from django.contrib.auth.models import User +import rest_framework.status as response_status +from lofardata.models import DataProduct, DataLocation + +test_object_value = dict(obs_id='12345', oid_source='SAS', dataproduct_source='lofar', + dataproduct_type='observation', + project='LT10_10', + location='srm://surfsara.nl:4884/', + activity='observation', + surl='srm://surfsara.nl:4884/...', + filesize=40, + additional_meta={'dysco_compression': True}) + + +class TestDatabaseInteraction(dtest.TestCase): + def test_insert(self): + location = DataLocation(name='surfsara', uri='srm://surfsara.nl') + location.save() + test_values = dict(test_object_value) + test_values['location'] = location + dp = DataProduct(**test_values) + dp.save() + self.assertTrue(dp.pk is not None, 'Failed saving object') + dp = DataProduct.objects.get(obs_id='12345') + for field in test_values: + self.assertEqual(getattr(dp, field), test_values.get(field)) + + def test_insert_with_helper_function(self): + test_values = dict(test_object_value) + test_values.pop('location') + + dp = DataProduct.insert_dataproduct(**test_values) + + for field in test_values: + self.assertEqual(test_object_value.get(field), getattr(dp, field), msg=f'Field {field} does not coincide') + + self.assertEqual('surfsara.nl:4884', dp.location.name) + self.assertEqual( 'srm://surfsara.nl:4884', dp.location.uri) + + +class TestRESTAPI(rtest.APITestCase): + def setUp(self): + self.client = rtest.APIClient() + self.user = User.objects.create_superuser('admin') + self.client.force_authenticate(self.user) + + def test_insert_not_allowed(self): + client = rtest.APIClient() + response = client.post('/ldvspec/data/insert/', data={}, format='json') + self.assertEqual(response_status.HTTP_403_FORBIDDEN, response.status_code) + + def test_insert_flat_error(self): + response = self.client.post('/ldvspec/data/insert/', data={}, format='json') + self.assertEqual(response_status.HTTP_400_BAD_REQUEST, response.status_code) + + def test_insert_flat_single(self): + test_payload = dict(test_object_value) + test_payload.pop('location') + response = self.client.post('/ldvspec/data/insert/', data=test_payload, format='json') + self.assertEqual(response_status.HTTP_201_CREATED, response.status_code) + + def test_insert_flat_multi(self): + test_payload = dict(test_object_value) + test_payload.pop('location') + response = self.client.post('/ldvspec/data/insert/', data=[test_payload, test_payload], format='json') + + self.assertEqual(response_status.HTTP_201_CREATED, response.status_code) + self.assertTrue(DataProduct.objects.count() == 2, 'Not all dataproduct have been inserted') + self.assertTrue(DataLocation.objects.count() == 1, 'Not all dataproduct have been inserted') + + def test_insert_flat_multi_insert_single(self): + test_payload = dict(test_object_value) + test_payload.pop('location') + response = self.client.post('/ldvspec/data/insert/', data=test_payload, format='json') + + self.assertEqual(response_status.HTTP_201_CREATED, response.status_code) + self.assertTrue(DataProduct.objects.count() == 1, 'Not all dataproduct have been inserted') + self.assertTrue(DataLocation.objects.count() == 1, 'Not all dataproduct have been inserted') \ No newline at end of file diff --git a/ldvspec/lofardata/tests/test_location.py b/ldvspec/lofardata/tests/test_location.py new file mode 100644 index 0000000000000000000000000000000000000000..93a10e82aede0e4461ab7adf480223153a134158 --- /dev/null +++ b/ldvspec/lofardata/tests/test_location.py @@ -0,0 +1,56 @@ +import django.test as dtest +import rest_framework.test as rtest +from django.contrib.auth.models import User +import rest_framework.status as response_status +from lofardata.models import DataLocation + + +class TestDataLocation(dtest.TestCase): + + def test_insert(self): + location = DataLocation(name='test', uri='srm://path_to_file/') + location.save() + + self.assertTrue(location.pk is not None, 'Cannot save object') + + def test_double_insert_unique_entry(self): + location = DataLocation(name='test', uri='srm://path_to_file/') + location.save() + + self.assertTrue(location.pk is not None, 'Cannot save object') + location_first_pk = location.pk + + location = DataLocation(name='test', uri='srm://path_to_file/') + location.save() + + self.assertTrue(location.pk is not None, 'Cannot save object') + location_second_pk = location.pk + + self.assertTrue(location_first_pk == location_second_pk, 'Double inserted index do not coincide') + + self.assertTrue(DataLocation.objects.count() == 1) + + def test_insert_from_string(self): + dataloc = DataLocation.insert_location_from_string('srm://test_site:44321/') + + self.assertTrue(dataloc.pk is not None, 'Cannot save object') + self.assertEqual('test_site:44321', dataloc.name) + self.assertEqual('srm://test_site:44321', dataloc.uri) + + +class TestRESTAPI(rtest.APITestCase): + def setUp(self): + self.client = rtest.APIClient() + self.user = User.objects.create_superuser('admin') + self.client.force_authenticate(self.user) + + def test_create_error(self): + response = self.client.post('/ldvspec/data-location/', data=dict(name='testname'), + format='json') + self.assertEqual(response_status.HTTP_400_BAD_REQUEST, response.status_code) + + def test_create(self): + response = self.client.post('/ldvspec/data-location/', data=dict(name='testname', uri='srm://myniceuri/'), + format='json') + self.assertEqual(response_status.HTTP_201_CREATED, response.status_code) + self.assertEqual('srm://myniceuri/', DataLocation.objects.get(name='testname').uri) \ No newline at end of file diff --git a/ldvspec/lofardata/urls.py b/ldvspec/lofardata/urls.py index 99257b63f946d7419d123ebfe0e89c14fc4cec47..e9c770eaceb6d20ce701f2840350487203486786 100644 --- a/ldvspec/lofardata/urls.py +++ b/ldvspec/lofardata/urls.py @@ -15,6 +15,9 @@ urlpatterns = [ # REST API path('data/', views.DataProductView.as_view(), name='dataproduct'), + path('data/insert/', views.InsertMultiDataproductView.as_view(), name='dataproduct-insert'), + path('data-location/', views.DataLocationView.as_view(), name='datalocation'), + path('openapi/', get_schema_view( title="LDV Specification", description="API description", diff --git a/ldvspec/lofardata/views.py b/ldvspec/lofardata/views.py index 92120167993ee234e0de3838aa144633d3e0e6c1..ef34e427c9357157e7495a4863da45eb0e27d2ef 100644 --- a/ldvspec/lofardata/views.py +++ b/ldvspec/lofardata/views.py @@ -1,12 +1,17 @@ +import logging + from django.shortcuts import render from django.conf import settings from rest_framework import generics, pagination +from rest_framework.views import APIView +from rest_framework import status +from rest_framework.response import Response from django_filters import rest_framework as filters -from .models import DataProduct, DataProductFilter -from .serializers import DataProductSerializer +from .models import DataProduct, DataProductFilter, DataLocation +from .serializers import DataProductSerializer, DataProductFlatSerializer, DataLocationSerializer class DynamicFilterSet(filters.FilterSet): @@ -18,16 +23,12 @@ class DynamicFilterSet(filters.FilterSet): super().__init__(*args, **kwargs) def _load_filters(self): - print('Current filters are', self.base_filters) if self.Meta.filter_class is None: raise Exception('Define filter_class meta attribute') - - print('Filter to be added', self.Meta.filter_class.objects.all()) for item in self.Meta.filter_class.objects.all(): field_obj = self.Meta.model._meta.get_field(item.field) filter_class, *_ = self.filter_for_lookup(field_obj, item.lookup_type) self.base_filters[item.name] = filter_class(item.field) - print('Final filters are', self.base_filters) # --- Filters --- @@ -48,7 +49,6 @@ def index(request): # ---------- REST API views ---------- - class DataProductView(generics.ListCreateAPIView): model = DataProduct serializer_class = DataProductSerializer @@ -59,5 +59,22 @@ class DataProductView(generics.ListCreateAPIView): filter_backends = (filters.DjangoFilterBackend,) filter_class = DataProductFilterSet().__class__ - def get_queryset(self): - return self.queryset.order_by('obs_id') + +class DataLocationView(generics.ListCreateAPIView): + model = DataLocation + serializer_class = DataLocationSerializer + queryset = DataLocation.objects.all().order_by('name') + + +class InsertMultiDataproductView(generics.CreateAPIView): + """ + Add single DataProduct + """ + queryset = DataProduct.objects.all() + serializer_class = DataProductFlatSerializer + + def get_serializer(self, *args, **kwargs): + """ if an array is passed, set serializer to many """ + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + return super().get_serializer(*args, **kwargs) diff --git a/ldvspec/manage.py b/ldvspec/manage.py index 110f50eed9bb0655751f09aeaca46b10b0507598..b99a0a4bbaf9cacd4d3a770bd1dd73a52b69b170 100644 --- a/ldvspec/manage.py +++ b/ldvspec/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ldvspec.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ldvspec.settings.dev') try: from django.core.management import execute_from_command_line except ImportError as exc: